Kaynağa Gözat

improve position charts

Daniel Bohry 3 ay önce
ebeveyn
işleme
13eab51b46

+ 277 - 90
src/components/CurrentPositionChart.svelte

@@ -1,13 +1,20 @@
 <script>
 	import { ArcElement, Chart, DoughnutController, Legend, Title, Tooltip } from 'chart.js';
+	import ChartDataLabels from 'chartjs-plugin-datalabels';
 	import { onDestroy } from 'svelte';
 
 	export let data = {};
 	export let currency = 'USD';
 	export let total = 0;
+	export let showDataLabels = true;
+	export let showCenterText = true;
+	export let height = '400px';
+	export let colors = null;
 
 	let chartContainer;
 	let chartInstance;
+	let isLoading = false;
+	let error = null;
 
 	function formatCurrency(value, fmtCurrency) {
 		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 = {
 		id: 'centerText',
 		beforeDraw(chart) {
 			const centerTextOptions = chart.options.plugins.centerText;
 			if (
 				!centerTextOptions ||
+				!centerTextOptions.enabled ||
 				centerTextOptions.displayTotal === undefined ||
 				!centerTextOptions.displayCurrency
 			) {
@@ -34,8 +76,11 @@
 			const { width, height, ctx } = chart;
 			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 computedColor = getComputedStyle(probe)?.color;
 			ctx.fillStyle = computedColor || '#333';
@@ -43,103 +88,157 @@
 			ctx.textAlign = 'center';
 			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();
 		}
 	};
 
-	Chart.register(ArcElement, Tooltip, Legend, Title, DoughnutController, centerTextPlugin);
+	Chart.register(ArcElement, Tooltip, Legend, Title, DoughnutController, ChartDataLabels, centerTextPlugin);
 
 	$: if (chartContainer && data?.stocks) {
 		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(() => {
@@ -149,18 +248,106 @@
 	});
 </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}
-		<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}
 </div>

+ 259 - 69
src/components/MarketDistributionChart.svelte

@@ -6,9 +6,15 @@
 	export let data = {};
 	export let currency = 'USD';
 	export let total = 0;
+	export let showDataLabels = true;
+	export let showCenterText = true;
+	export let height = '400px';
+	export let colors = null; // Allow custom colors
 
 	let chartContainer;
 	let chartInstance;
+	let isLoading = false;
+	let error = null;
 
 	function formatCurrency(value, fmtCurrency) {
 		return value.toLocaleString('en-US', {
@@ -17,12 +23,32 @@
 		});
 	}
 
+	// Generate colors for market distribution (typically 4 markets: Brazil, Germany, USA, Others)
+	function generateColors(count) {
+		if (colors && Array.isArray(colors) && colors.length >= count) {
+			return colors.slice(0, count);
+		}
+
+		// Default colors for markets (same beautiful palette as CurrentPositionChart)
+		const defaultColors = [
+			'#1abc9c', // Brazil
+			'#2ecc71', // Germany
+			'#3498db', // USA
+			'#9b59b6', // Others
+			'#f1c40f',
+			'#e67e22'
+		];
+
+		return defaultColors.slice(0, count);
+	}
+
 	const centerTextPlugin = {
 		id: 'centerText',
 		beforeDraw(chart) {
 			const centerTextOptions = chart.options.plugins.centerText;
 			if (
 				!centerTextOptions ||
+				!centerTextOptions.enabled ||
 				centerTextOptions.displayTotal === undefined ||
 				!centerTextOptions.displayCurrency
 			) {
@@ -35,8 +61,11 @@
 			const { width, height, ctx } = chart;
 			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 computedColor = getComputedStyle(probe)?.color;
 			ctx.fillStyle = computedColor || '#333';
@@ -44,8 +73,19 @@
 			ctx.textAlign = 'center';
 			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();
 		}
@@ -86,73 +126,132 @@
 		return groups;
 	};
 
-	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 grouped = groupByMarket(data.stocks);
-		const labels = Object.keys(grouped);
-		const values = Object.values(grouped);
-
-		const chartData = {
-			labels,
-			datasets: [
-				{
-					data: values,
-					backgroundColor: ['#1abc9c', '#2ecc71', '#3498db', '#9b59b6', '#f1c40f', '#e67e22'],
-					borderColor: '#fff',
-					borderWidth: 1
+			if (!data || !data.stocks || data.stocks.length === 0) {
+				if (chartInstance) {
+					chartInstance.destroy();
+					chartInstance = null;
 				}
-			]
-		};
+				return;
+			}
 
-		const chartOptions = {
-			responsive: true,
-			maintainAspectRatio: false,
-			plugins: {
-				legend: {
-					display: false
-				},
-				tooltip: {
-					callbacks: {
-						label: function (tooltipItem) {
-							const value = tooltipItem.raw;
-							const percentage = ((value / total) * 100).toFixed(2);
-							return `${percentage}% (${formatCurrency(value, currency)})`;
-						}
+			const grouped = groupByMarket(data.stocks);
+			const labels = Object.keys(grouped);
+			const values = Object.values(grouped);
+			const chartColors = generateColors(labels.length);
+
+			const chartData = {
+				labels,
+				datasets: [
+					{
+						label: 'Market Distribution',
+						data: values,
+						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: {
-						weight: 'bold'
+				plugins: {
+					legend: {
+						display: false,
+						position: 'bottom',
+						labels: {
+							boxWidth: 12,
+							padding: 15,
+							usePointStyle: true
+						}
 					},
-					formatter: (value) => {
-						const percentage = (value / total) * 100;
-						if (percentage < 0.1) {
-							return '';
+					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 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;
+
+							// Only show labels for segments larger than 0.1%
+							if (percentage < 0.1) {
+								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;
+		}
 	}
 
 	$: if (chartContainer && data?.stocks?.length) {
@@ -166,15 +265,106 @@
 	});
 </script>
 
-<!-- Chart container -->
-<div class="max-w-md mx-auto relative" style="height: 400px;">
-	{#if data?.stocks?.length}
-		<p class="text-center font-semibold text-gray-700 dark:text-gray-300 mb-2 text-base">
-			Market Distribution
-		</p>
-		<span id="chart-text-color" class="hidden text-gray-900 dark:text-gray-100"></span>
-		<canvas bind:this={chartContainer} class="w-full h-full"></canvas>
+<!-- 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="Market distribution chart showing portfolio allocation across different markets"
+>
+	{#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?.stocks?.length}
+		<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">
+				Market Distribution
+			</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 market distribution: {Object.entries(groupByMarket(data.stocks)).filter(([_, value]) => value > 0).map(([market, value]) => `${market}: ${((value / total) * 100).toFixed(1)}%`).join(', ')}"
+					tabindex="0"
+				></canvas>
+			</div>
+
+			<!-- Screen reader accessible data table -->
+			<div class="sr-only">
+				<table>
+					<caption>Market Distribution Details</caption>
+					<thead>
+						<tr>
+							<th scope="col">Market</th>
+							<th scope="col">Value</th>
+							<th scope="col">Percentage</th>
+						</tr>
+					</thead>
+					<tbody>
+						{#each Object.entries(groupByMarket(data.stocks)).filter(([_, value]) => value > 0) as [market, value]}
+							<tr>
+								<td>{market}</td>
+								<td>{formatCurrency(value, currency)}</td>
+								<td>{((value / total) * 100).toFixed(2)}%</td>
+							</tr>
+						{/each}
+					</tbody>
+				</table>
+			</div>
+
+			<!-- Legend for mobile devices (typically 4 markets or less) -->
+			{#if Object.values(groupByMarket(data.stocks)).filter(v => v > 0).length <= 4}
+				<div class="mt-4 grid grid-cols-2 gap-2 text-xs md:hidden" aria-label="Chart legend">
+					{#each Object.entries(groupByMarket(data.stocks)).filter(([_, value]) => value > 0) as [market, value], index}
+						<div class="flex items-center space-x-2">
+							<div
+								class="w-3 h-3 rounded-full flex-shrink-0"
+								style="background-color: {generateColors(Object.keys(groupByMarket(data.stocks)).length)[index]}"
+								aria-hidden="true"
+							></div>
+							<span class="text-gray-700 dark:text-gray-300 truncate">
+								{market}
+							</span>
+						</div>
+					{/each}
+				</div>
+			{/if}
+		</div>
 	{:else}
-		<p class="text-center text-gray-500 dark:text-gray-400">No data 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="M3.055 11H5a2 2 0 012 2v1a2 2 0 002 2 2 2 0 012 2v2.945M8 3.935V5.5A2.5 2.5 0 0010.5 8h.5a2 2 0 012 2 2 2 0 104 0 2 2 0 012-2h1.064M15 20.488V18a2 2 0 012-2h3.064M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
+					</svg>
+				</div>
+				<p class="text-center text-gray-500 dark:text-gray-400">
+					No market data available
+				</p>
+			</div>
+		</div>
 	{/if}
 </div>

+ 4 - 7
src/components/PortfolioValueHistory.svelte

@@ -1,10 +1,9 @@
 <script>
-	import { onMount } from 'svelte';
-	import { Chart, registerables } from 'chart.js';
-	import { getRequest } from '../utils/api.js';
-	import { fade } from 'svelte/transition';
+    import {Chart, registerables} from 'chart.js';
+    import {getRequest} from '../utils/api.js';
+    import {fade} from 'svelte/transition';
 
-	Chart.register(...registerables);
+    Chart.register(...registerables);
 
 	export let portfolioId;
 	export let authToken;
@@ -25,10 +24,8 @@
 			historyData = data;
 
 			if (data.length > 0) {
-				// Sort by date to ensure proper order
 				historyData = data.sort((a, b) => new Date(a.createdAt) - new Date(b.createdAt));
 
-				// Calculate value change
 				const firstValue = historyData[0].totalValue;
 				const lastValue = historyData[historyData.length - 1].totalValue;
 				currentValue = lastValue;