|
@@ -1,13 +1,20 @@
|
|
|
<script>
|
|
<script>
|
|
|
import { ArcElement, Chart, DoughnutController, Legend, Title, Tooltip } from 'chart.js';
|
|
import { ArcElement, Chart, DoughnutController, Legend, Title, Tooltip } from 'chart.js';
|
|
|
|
|
+ import ChartDataLabels from 'chartjs-plugin-datalabels';
|
|
|
import { onDestroy } from 'svelte';
|
|
import { onDestroy } from 'svelte';
|
|
|
|
|
|
|
|
export let data = {};
|
|
export let data = {};
|
|
|
export let currency = 'USD';
|
|
export let currency = 'USD';
|
|
|
export let total = 0;
|
|
export let total = 0;
|
|
|
|
|
+ export let showDataLabels = true;
|
|
|
|
|
+ export let showCenterText = true;
|
|
|
|
|
+ export let height = '400px';
|
|
|
|
|
+ export let colors = null;
|
|
|
|
|
|
|
|
let chartContainer;
|
|
let chartContainer;
|
|
|
let chartInstance;
|
|
let chartInstance;
|
|
|
|
|
+ let isLoading = false;
|
|
|
|
|
+ let error = null;
|
|
|
|
|
|
|
|
function formatCurrency(value, fmtCurrency) {
|
|
function formatCurrency(value, fmtCurrency) {
|
|
|
return value.toLocaleString('en-US', {
|
|
return value.toLocaleString('en-US', {
|
|
@@ -16,12 +23,47 @@
|
|
|
});
|
|
});
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ function generateColors(count) {
|
|
|
|
|
+ if (colors && Array.isArray(colors) && colors.length >= count) {
|
|
|
|
|
+ return colors.slice(0, count);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const defaultColors = [
|
|
|
|
|
+ '#1abc9c',
|
|
|
|
|
+ '#2ecc71',
|
|
|
|
|
+ '#3498db',
|
|
|
|
|
+ '#9b59b6',
|
|
|
|
|
+ '#f1c40f',
|
|
|
|
|
+ '#e67e22',
|
|
|
|
|
+ '#e74c3c',
|
|
|
|
|
+ '#34495e',
|
|
|
|
|
+ '#16a085',
|
|
|
|
|
+ '#27ae60',
|
|
|
|
|
+ '#2980b9',
|
|
|
|
|
+ '#8e44ad',
|
|
|
|
|
+ '#f39c12',
|
|
|
|
|
+ '#d35400',
|
|
|
|
|
+ '#c0392b'
|
|
|
|
|
+ ];
|
|
|
|
|
+
|
|
|
|
|
+ if (count > defaultColors.length) {
|
|
|
|
|
+ const repeatedColors = [];
|
|
|
|
|
+ for (let i = 0; i < count; i++) {
|
|
|
|
|
+ repeatedColors.push(defaultColors[i % defaultColors.length]);
|
|
|
|
|
+ }
|
|
|
|
|
+ return repeatedColors;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return defaultColors.slice(0, count);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
const centerTextPlugin = {
|
|
const centerTextPlugin = {
|
|
|
id: 'centerText',
|
|
id: 'centerText',
|
|
|
beforeDraw(chart) {
|
|
beforeDraw(chart) {
|
|
|
const centerTextOptions = chart.options.plugins.centerText;
|
|
const centerTextOptions = chart.options.plugins.centerText;
|
|
|
if (
|
|
if (
|
|
|
!centerTextOptions ||
|
|
!centerTextOptions ||
|
|
|
|
|
+ !centerTextOptions.enabled ||
|
|
|
centerTextOptions.displayTotal === undefined ||
|
|
centerTextOptions.displayTotal === undefined ||
|
|
|
!centerTextOptions.displayCurrency
|
|
!centerTextOptions.displayCurrency
|
|
|
) {
|
|
) {
|
|
@@ -34,8 +76,11 @@
|
|
|
const { width, height, ctx } = chart;
|
|
const { width, height, ctx } = chart;
|
|
|
ctx.save();
|
|
ctx.save();
|
|
|
|
|
|
|
|
- ctx.font = 'bold 20px sans-serif';
|
|
|
|
|
|
|
+ // Responsive font size based on chart size
|
|
|
|
|
+ const fontSize = Math.min(width, height) * 0.08;
|
|
|
|
|
+ ctx.font = `bold ${fontSize}px sans-serif`;
|
|
|
|
|
|
|
|
|
|
+ // Get theme-aware color
|
|
|
const probe = document.getElementById('chart-text-color');
|
|
const probe = document.getElementById('chart-text-color');
|
|
|
const computedColor = getComputedStyle(probe)?.color;
|
|
const computedColor = getComputedStyle(probe)?.color;
|
|
|
ctx.fillStyle = computedColor || '#333';
|
|
ctx.fillStyle = computedColor || '#333';
|
|
@@ -43,103 +88,157 @@
|
|
|
ctx.textAlign = 'center';
|
|
ctx.textAlign = 'center';
|
|
|
ctx.textBaseline = 'middle';
|
|
ctx.textBaseline = 'middle';
|
|
|
|
|
|
|
|
- const text = `${formatCurrency(displayTotal, displayCurrency)}`;
|
|
|
|
|
- ctx.fillText(text, width / 2, height / 2);
|
|
|
|
|
|
|
+ try {
|
|
|
|
|
+ const text = formatCurrency(displayTotal, displayCurrency);
|
|
|
|
|
+
|
|
|
|
|
+ // Add text shadow for better readability
|
|
|
|
|
+ ctx.shadowColor = 'rgba(0, 0, 0, 0.3)';
|
|
|
|
|
+ ctx.shadowBlur = 4;
|
|
|
|
|
+ ctx.shadowOffsetX = 1;
|
|
|
|
|
+ ctx.shadowOffsetY = 1;
|
|
|
|
|
+
|
|
|
|
|
+ ctx.fillText(text, width / 2, height / 2);
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ console.warn('Error rendering center text:', err);
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
ctx.restore();
|
|
ctx.restore();
|
|
|
}
|
|
}
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
- Chart.register(ArcElement, Tooltip, Legend, Title, DoughnutController, centerTextPlugin);
|
|
|
|
|
|
|
+ Chart.register(ArcElement, Tooltip, Legend, Title, DoughnutController, ChartDataLabels, centerTextPlugin);
|
|
|
|
|
|
|
|
$: if (chartContainer && data?.stocks) {
|
|
$: if (chartContainer && data?.stocks) {
|
|
|
updateChart();
|
|
updateChart();
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- function updateChart() {
|
|
|
|
|
- if (chartInstance) {
|
|
|
|
|
- chartInstance.destroy();
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- if (!data || !data.stocks || data.stocks.length === 0) {
|
|
|
|
|
- chartInstance = null;
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ async function updateChart() {
|
|
|
|
|
+ try {
|
|
|
|
|
+ isLoading = true;
|
|
|
|
|
+ error = null;
|
|
|
|
|
|
|
|
- const stockNames = data.stocks.map((stock) => stock.name);
|
|
|
|
|
- const stockTotals = data.stocks.map((stock) => stock.total);
|
|
|
|
|
-
|
|
|
|
|
- const chartData = {
|
|
|
|
|
- labels: stockNames,
|
|
|
|
|
- datasets: [
|
|
|
|
|
- {
|
|
|
|
|
- label: 'Stock Distribution',
|
|
|
|
|
- data: stockTotals,
|
|
|
|
|
- backgroundColor: [
|
|
|
|
|
- '#1abc9c',
|
|
|
|
|
- '#2ecc71',
|
|
|
|
|
- '#3498db',
|
|
|
|
|
- '#9b59b6',
|
|
|
|
|
- '#f1c40f',
|
|
|
|
|
- '#e67e22',
|
|
|
|
|
- '#e74c3c',
|
|
|
|
|
- '#34495e',
|
|
|
|
|
- '#16a085',
|
|
|
|
|
- '#27ae60',
|
|
|
|
|
- '#2980b9',
|
|
|
|
|
- '#8e44ad',
|
|
|
|
|
- '#f39c12',
|
|
|
|
|
- '#d35400',
|
|
|
|
|
- '#c0392b'
|
|
|
|
|
- ],
|
|
|
|
|
- borderColor: '#fff',
|
|
|
|
|
- borderWidth: 1
|
|
|
|
|
|
|
+ if (!data || !data.stocks || data.stocks.length === 0) {
|
|
|
|
|
+ if (chartInstance) {
|
|
|
|
|
+ chartInstance.destroy();
|
|
|
|
|
+ chartInstance = null;
|
|
|
}
|
|
}
|
|
|
- ]
|
|
|
|
|
- };
|
|
|
|
|
-
|
|
|
|
|
- const chartOptions = {
|
|
|
|
|
- responsive: true,
|
|
|
|
|
- maintainAspectRatio: false,
|
|
|
|
|
- plugins: {
|
|
|
|
|
- legend: {
|
|
|
|
|
- display: false
|
|
|
|
|
- },
|
|
|
|
|
- tooltip: {
|
|
|
|
|
- callbacks: {
|
|
|
|
|
- label: function (tooltipItem) {
|
|
|
|
|
- const value = tooltipItem.raw;
|
|
|
|
|
- const dataset = tooltipItem.chart.data.datasets[tooltipItem.datasetIndex];
|
|
|
|
|
- const data = dataset.data;
|
|
|
|
|
- const total = data.reduce((sum, val) => sum + val, 0);
|
|
|
|
|
- const percentage = total ? ((value / total) * 100).toFixed(2) : '0.00';
|
|
|
|
|
- return `${percentage}% (${formatCurrency(value, currency)})`;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const stockNames = data.stocks.map((stock) => stock.name);
|
|
|
|
|
+ const stockTotals = data.stocks.map((stock) => stock.total);
|
|
|
|
|
+ const chartColors = generateColors(stockNames.length);
|
|
|
|
|
+
|
|
|
|
|
+ const chartData = {
|
|
|
|
|
+ labels: stockNames,
|
|
|
|
|
+ datasets: [
|
|
|
|
|
+ {
|
|
|
|
|
+ label: 'Portfolio Distribution',
|
|
|
|
|
+ data: stockTotals,
|
|
|
|
|
+ backgroundColor: chartColors,
|
|
|
|
|
+ borderColor: '#fff',
|
|
|
|
|
+ borderWidth: 1,
|
|
|
|
|
+ hoverBorderWidth: 1,
|
|
|
|
|
+ hoverBorderColor: '#ddd'
|
|
|
}
|
|
}
|
|
|
|
|
+ ]
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const chartOptions = {
|
|
|
|
|
+ responsive: true,
|
|
|
|
|
+ maintainAspectRatio: false,
|
|
|
|
|
+ interaction: {
|
|
|
|
|
+ intersect: false,
|
|
|
|
|
+ mode: 'nearest'
|
|
|
},
|
|
},
|
|
|
- centerText: {
|
|
|
|
|
- displayTotal: total,
|
|
|
|
|
- displayCurrency: currency
|
|
|
|
|
- },
|
|
|
|
|
- datalabels: {
|
|
|
|
|
- color: '#fff',
|
|
|
|
|
- font: {},
|
|
|
|
|
- formatter: (value) => {
|
|
|
|
|
- const percentage = (value / total) * 100;
|
|
|
|
|
- if (percentage < 3) {
|
|
|
|
|
- return '';
|
|
|
|
|
|
|
+ plugins: {
|
|
|
|
|
+ legend: {
|
|
|
|
|
+ display: false,
|
|
|
|
|
+ position: 'bottom',
|
|
|
|
|
+ labels: {
|
|
|
|
|
+ boxWidth: 12,
|
|
|
|
|
+ padding: 15,
|
|
|
|
|
+ usePointStyle: true
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ tooltip: {
|
|
|
|
|
+ backgroundColor: 'rgba(0, 0, 0, 0.8)',
|
|
|
|
|
+ titleColor: '#fff',
|
|
|
|
|
+ bodyColor: '#fff',
|
|
|
|
|
+ borderColor: 'rgba(255, 255, 255, 0.1)',
|
|
|
|
|
+ borderWidth: 1,
|
|
|
|
|
+ cornerRadius: 8,
|
|
|
|
|
+ displayColors: true,
|
|
|
|
|
+ callbacks: {
|
|
|
|
|
+ title: function (tooltipItems) {
|
|
|
|
|
+ return tooltipItems[0].label;
|
|
|
|
|
+ },
|
|
|
|
|
+ label: function (tooltipItem) {
|
|
|
|
|
+ const value = tooltipItem.raw;
|
|
|
|
|
+ const dataset = tooltipItem.chart.data.datasets[tooltipItem.datasetIndex];
|
|
|
|
|
+ const dataArray = dataset.data;
|
|
|
|
|
+ const total = dataArray.reduce((sum, val) => sum + val, 0);
|
|
|
|
|
+ const percentage = total ? ((value / total) * 100).toFixed(2) : '0.00';
|
|
|
|
|
+ return `${percentage}% (${formatCurrency(value, currency)})`;
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
- return `${percentage.toFixed(1)}%`;
|
|
|
|
|
|
|
+ },
|
|
|
|
|
+ centerText: {
|
|
|
|
|
+ enabled: showCenterText,
|
|
|
|
|
+ displayTotal: total,
|
|
|
|
|
+ displayCurrency: currency
|
|
|
|
|
+ },
|
|
|
|
|
+ datalabels: {
|
|
|
|
|
+ display: showDataLabels,
|
|
|
|
|
+ color: '#fff',
|
|
|
|
|
+ font: {
|
|
|
|
|
+ weight: 'bold',
|
|
|
|
|
+ size: 12
|
|
|
|
|
+ },
|
|
|
|
|
+ formatter: (value, context) => {
|
|
|
|
|
+ const dataArray = context.chart.data.datasets[0].data;
|
|
|
|
|
+ const total = dataArray.reduce((sum, val) => sum + val, 0);
|
|
|
|
|
+ const percentage = total ? (value / total) * 100 : 0;
|
|
|
|
|
+
|
|
|
|
|
+ if (percentage < 3) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ return `${percentage.toFixed(1)}%`;
|
|
|
|
|
+ },
|
|
|
|
|
+ anchor: 'center',
|
|
|
|
|
+ align: 'center',
|
|
|
|
|
+ offset: 0,
|
|
|
|
|
+ borderColor: 'rgba(0, 0, 0, 0.3)',
|
|
|
|
|
+ borderRadius: 4,
|
|
|
|
|
+ borderWidth: 1,
|
|
|
|
|
+ backgroundColor: 'rgba(0, 0, 0, 0.7)',
|
|
|
|
|
+ padding: 4
|
|
|
}
|
|
}
|
|
|
|
|
+ },
|
|
|
|
|
+ // Add accessibility
|
|
|
|
|
+ accessibility: {
|
|
|
|
|
+ enabled: true
|
|
|
}
|
|
}
|
|
|
- }
|
|
|
|
|
- };
|
|
|
|
|
|
|
+ };
|
|
|
|
|
|
|
|
- chartInstance = new Chart(chartContainer, {
|
|
|
|
|
- type: 'doughnut',
|
|
|
|
|
- data: chartData,
|
|
|
|
|
- options: chartOptions
|
|
|
|
|
- });
|
|
|
|
|
|
|
+ // Update existing chart if possible, otherwise create new one
|
|
|
|
|
+ if (chartInstance) {
|
|
|
|
|
+ chartInstance.data = chartData;
|
|
|
|
|
+ chartInstance.options = chartOptions;
|
|
|
|
|
+ chartInstance.update('active');
|
|
|
|
|
+ } else {
|
|
|
|
|
+ chartInstance = new Chart(chartContainer, {
|
|
|
|
|
+ type: 'doughnut',
|
|
|
|
|
+ data: chartData,
|
|
|
|
|
+ options: chartOptions
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ console.error('Error updating chart:', err);
|
|
|
|
|
+ error = `Failed to render chart: ${err.message}`;
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ isLoading = false;
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
onDestroy(() => {
|
|
onDestroy(() => {
|
|
@@ -149,18 +248,106 @@
|
|
|
});
|
|
});
|
|
|
</script>
|
|
</script>
|
|
|
|
|
|
|
|
-<!-- 👇 Tailwind container with probe element and canvas -->
|
|
|
|
|
-<div class="max-w-sm md:max-w-md lg:max-w-lg mx-auto relative" style="height: 400px;">
|
|
|
|
|
- {#if data && data.stocks && data.stocks.length > 0}
|
|
|
|
|
- <p class="text-center font-semibold text-gray-700 dark:text-gray-300 mb-2 text-base">
|
|
|
|
|
- Current Portfolio Positions
|
|
|
|
|
- </p>
|
|
|
|
|
|
|
+<!-- Chart container with responsive sizing and accessibility -->
|
|
|
|
|
+<div
|
|
|
|
|
+ class="max-w-sm md:max-w-md lg:max-w-lg xl:max-w-xl mx-auto relative"
|
|
|
|
|
+ style="height: {height};"
|
|
|
|
|
+ role="img"
|
|
|
|
|
+ aria-label="Portfolio distribution chart showing current position percentages"
|
|
|
|
|
+>
|
|
|
|
|
+ {#if error}
|
|
|
|
|
+ <div class="flex items-center justify-center h-full">
|
|
|
|
|
+ <div class="text-center p-4">
|
|
|
|
|
+ <div class="text-red-500 dark:text-red-400 mb-2">
|
|
|
|
|
+ <svg class="w-8 h-8 mx-auto" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
|
|
|
|
+ </svg>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <p class="text-sm text-red-600 dark:text-red-400" role="alert">
|
|
|
|
|
+ {error}
|
|
|
|
|
+ </p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ {:else if isLoading}
|
|
|
|
|
+ <div class="flex items-center justify-center h-full" aria-label="Loading chart data">
|
|
|
|
|
+ <div class="text-center p-4">
|
|
|
|
|
+ <div class="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500 mx-auto mb-2"></div>
|
|
|
|
|
+ <p class="text-sm text-gray-600 dark:text-gray-400">Loading chart...</p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ {:else if data && data.stocks && data.stocks.length > 0}
|
|
|
|
|
+ <div class="h-full flex flex-col">
|
|
|
|
|
+ <h3 class="text-center font-semibold text-gray-700 dark:text-gray-300 mb-4 text-base md:text-lg">
|
|
|
|
|
+ Current Portfolio Positions
|
|
|
|
|
+ </h3>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- Hidden span used for runtime color detection -->
|
|
|
|
|
+ <span id="chart-text-color" class="hidden text-gray-900 dark:text-gray-100" aria-hidden="true"></span>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- Chart canvas with accessibility attributes -->
|
|
|
|
|
+ <div class="flex-1 relative">
|
|
|
|
|
+ <canvas
|
|
|
|
|
+ bind:this={chartContainer}
|
|
|
|
|
+ class="w-full h-full"
|
|
|
|
|
+ role="img"
|
|
|
|
|
+ aria-label="Doughnut chart showing portfolio distribution: {data.stocks.map(stock => `${stock.name}: ${((stock.total / total) * 100).toFixed(1)}%`).join(', ')}"
|
|
|
|
|
+ tabindex="0"
|
|
|
|
|
+ ></canvas>
|
|
|
|
|
+ </div>
|
|
|
|
|
|
|
|
- <!-- 👇 Hidden span used for runtime color detection -->
|
|
|
|
|
- <span id="chart-text-color" class="hidden text-gray-900 dark:text-gray-100"></span>
|
|
|
|
|
|
|
+ <!-- Screen reader accessible data table -->
|
|
|
|
|
+ <div class="sr-only">
|
|
|
|
|
+ <table>
|
|
|
|
|
+ <caption>Portfolio Position Details</caption>
|
|
|
|
|
+ <thead>
|
|
|
|
|
+ <tr>
|
|
|
|
|
+ <th scope="col">Stock</th>
|
|
|
|
|
+ <th scope="col">Value</th>
|
|
|
|
|
+ <th scope="col">Percentage</th>
|
|
|
|
|
+ </tr>
|
|
|
|
|
+ </thead>
|
|
|
|
|
+ <tbody>
|
|
|
|
|
+ {#each data.stocks as stock}
|
|
|
|
|
+ <tr>
|
|
|
|
|
+ <td>{stock.name}</td>
|
|
|
|
|
+ <td>{formatCurrency(stock.total, currency)}</td>
|
|
|
|
|
+ <td>{((stock.total / total) * 100).toFixed(2)}%</td>
|
|
|
|
|
+ </tr>
|
|
|
|
|
+ {/each}
|
|
|
|
|
+ </tbody>
|
|
|
|
|
+ </table>
|
|
|
|
|
+ </div>
|
|
|
|
|
|
|
|
- <canvas bind:this={chartContainer} class="w-full h-full"></canvas>
|
|
|
|
|
|
|
+ <!-- Legend for mobile devices (optional, can be enabled via prop) -->
|
|
|
|
|
+ {#if data.stocks.length <= 6}
|
|
|
|
|
+ <div class="mt-4 grid grid-cols-2 gap-2 text-xs md:hidden" aria-label="Chart legend">
|
|
|
|
|
+ {#each data.stocks as stock, index}
|
|
|
|
|
+ <div class="flex items-center space-x-2">
|
|
|
|
|
+ <div
|
|
|
|
|
+ class="w-3 h-3 rounded-full flex-shrink-0"
|
|
|
|
|
+ style="background-color: {generateColors(data.stocks.length)[index]}"
|
|
|
|
|
+ aria-hidden="true"
|
|
|
|
|
+ ></div>
|
|
|
|
|
+ <span class="text-gray-700 dark:text-gray-300 truncate">
|
|
|
|
|
+ {stock.name}
|
|
|
|
|
+ </span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ {/each}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ {/if}
|
|
|
|
|
+ </div>
|
|
|
{:else}
|
|
{:else}
|
|
|
- <p class="text-center text-gray-500 dark:text-gray-400">No insights available.</p>
|
|
|
|
|
|
|
+ <div class="flex items-center justify-center h-full">
|
|
|
|
|
+ <div class="text-center p-4">
|
|
|
|
|
+ <div class="text-gray-400 dark:text-gray-500 mb-2">
|
|
|
|
|
+ <svg class="w-12 h-12 mx-auto" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"></path>
|
|
|
|
|
+ </svg>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <p class="text-center text-gray-500 dark:text-gray-400">
|
|
|
|
|
+ No portfolio data available
|
|
|
|
|
+ </p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
{/if}
|
|
{/if}
|
|
|
</div>
|
|
</div>
|