mirror of https://github.com/ghostfolio/ghostfolio
11 changed files with 1254 additions and 21 deletions
@ -0,0 +1,333 @@ |
|||
<!doctype html> |
|||
<html lang="en"> |
|||
<head> |
|||
<meta charset="UTF-8" /> |
|||
<meta content="width=device-width, initial-scale=1.0" name="viewport" /> |
|||
<title>Ghostfolio Chart Improvements Demo</title> |
|||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script> |
|||
<script src="https://cdn.jsdelivr.net/npm/date-fns@2.29.3/index.min.js"></script> |
|||
<script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns@3.0.0/dist/chartjs-adapter-date-fns.bundle.min.js"></script> |
|||
<style> |
|||
body { |
|||
font-family: 'Inter', sans-serif; |
|||
margin: 0; |
|||
padding: 20px; |
|||
background-color: #f5f5f5; |
|||
} |
|||
.container { |
|||
max-width: 1200px; |
|||
margin: 0 auto; |
|||
} |
|||
.chart-container { |
|||
background: white; |
|||
border-radius: 8px; |
|||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); |
|||
padding: 20px; |
|||
margin-bottom: 30px; |
|||
} |
|||
h1, |
|||
h2 { |
|||
color: #1976d2; |
|||
} |
|||
.description { |
|||
background: #e3f2fd; |
|||
border-left: 4px solid #1976d2; |
|||
padding: 15px; |
|||
margin: 20px 0; |
|||
border-radius: 0 4px 4px 0; |
|||
} |
|||
.comparison { |
|||
display: flex; |
|||
gap: 20px; |
|||
margin: 30px 0; |
|||
} |
|||
.comparison-panel { |
|||
flex: 1; |
|||
padding: 20px; |
|||
border-radius: 8px; |
|||
} |
|||
.before { |
|||
background: #ffebee; |
|||
border: 1px solid #ffcdd2; |
|||
} |
|||
.after { |
|||
background: #e8f5e9; |
|||
border: 1px solid #c8e6c9; |
|||
} |
|||
.chart-wrapper { |
|||
height: 400px; |
|||
position: relative; |
|||
} |
|||
canvas { |
|||
width: 100% !important; |
|||
height: 100% !important; |
|||
} |
|||
</style> |
|||
</head> |
|||
<body> |
|||
<div class="container"> |
|||
<h1>Ghostfolio Chart Improvements Demo</h1> |
|||
|
|||
<div class="description"> |
|||
<h2>Visual Improvements for Portfolio Analysis Charts</h2> |
|||
<p> |
|||
This demo shows the improvements made to chart visual comparability in |
|||
the Portfolio Analysis section. The key enhancements include: |
|||
</p> |
|||
<ul> |
|||
<li> |
|||
<strong>Date Range Filtering:</strong> Charts now properly scale to |
|||
show only data within the selected time period |
|||
</li> |
|||
<li> |
|||
<strong>Enhanced Visual Design:</strong> Improved color schemes, |
|||
better spacing, and clearer data representation |
|||
</li> |
|||
<li> |
|||
<strong>Better Readability:</strong> Cleaner axes, grid lines, and |
|||
tooltips for easier analysis |
|||
</li> |
|||
</ul> |
|||
</div> |
|||
|
|||
<h2>Portfolio Performance Comparison</h2> |
|||
|
|||
<div class="comparison"> |
|||
<div class="comparison-panel before"> |
|||
<h3>Before: Truncated/Unscaled Charts</h3> |
|||
<p> |
|||
Charts were not properly scaled when applying time period filters, |
|||
making data difficult to read. |
|||
</p> |
|||
<div class="chart-wrapper"> |
|||
<canvas id="chartBefore"></canvas> |
|||
</div> |
|||
</div> |
|||
|
|||
<div class="comparison-panel after"> |
|||
<h3>After: Properly Scaled Charts</h3> |
|||
<p> |
|||
Charts now automatically adjust to show only the data within the |
|||
selected time period with proper scaling. |
|||
</p> |
|||
<div class="chart-wrapper"> |
|||
<canvas id="chartAfter"></canvas> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<h2>Investment Timeline Comparison</h2> |
|||
|
|||
<div class="chart-container"> |
|||
<h3>Enhanced Investment Timeline Chart</h3> |
|||
<p> |
|||
Improved visualization with better color coding and clearer data |
|||
representation. |
|||
</p> |
|||
<div class="chart-wrapper"> |
|||
<canvas id="investmentChart"></canvas> |
|||
</div> |
|||
</div> |
|||
|
|||
<h2>Dividend Timeline Comparison</h2> |
|||
|
|||
<div class="chart-container"> |
|||
<h3>Enhanced Dividend Timeline Chart</h3> |
|||
<p> |
|||
Better visual distinction for dividend data with improved readability. |
|||
</p> |
|||
<div class="chart-wrapper"> |
|||
<canvas id="dividendChart"></canvas> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<script> |
|||
// Generate mock data |
|||
function generateMockData(days = 365, startDate = new Date()) { |
|||
const data = []; |
|||
let value = 10000; |
|||
|
|||
for (let i = days; i >= 0; i--) { |
|||
const date = new Date(startDate); |
|||
date.setDate(date.getDate() - i); |
|||
|
|||
// Add some realistic fluctuations |
|||
const change = (Math.random() - 0.5) * 200; |
|||
value += change; |
|||
|
|||
data.push({ |
|||
date: date.toISOString().split('T')[0], |
|||
value: value |
|||
}); |
|||
} |
|||
|
|||
return data; |
|||
} |
|||
|
|||
// Generate investment data |
|||
function generateInvestmentData(months = 24) { |
|||
const data = []; |
|||
const today = new Date(); |
|||
|
|||
for (let i = months; i >= 0; i--) { |
|||
const date = new Date(today); |
|||
date.setMonth(date.getMonth() - i); |
|||
|
|||
const investment = Math.random() * 2000 + 500; |
|||
|
|||
data.push({ |
|||
date: date.toISOString().split('T')[0], |
|||
investment: investment |
|||
}); |
|||
} |
|||
|
|||
return data; |
|||
} |
|||
|
|||
// Generate dividend data |
|||
function generateDividendData(months = 24) { |
|||
const data = []; |
|||
const today = new Date(); |
|||
|
|||
for (let i = months; i >= 0; i--) { |
|||
const date = new Date(today); |
|||
date.setMonth(date.getMonth() - i); |
|||
|
|||
const dividend = Math.random() * 500 + 100; |
|||
|
|||
data.push({ |
|||
date: date.toISOString().split('T')[0], |
|||
investment: dividend |
|||
}); |
|||
} |
|||
|
|||
return data; |
|||
} |
|||
|
|||
// Create a chart with the given configuration |
|||
function createChart(canvasId, data, title, isInvestment = false) { |
|||
const ctx = document.getElementById(canvasId).getContext('2d'); |
|||
|
|||
// Filter data for last 6 months for "after" chart |
|||
const sixMonthsAgo = new Date(); |
|||
sixMonthsAgo.setMonth(sixMonthsAgo.getMonth() - 6); |
|||
|
|||
let filteredData = data; |
|||
if (canvasId === 'chartAfter') { |
|||
filteredData = data.filter( |
|||
(item) => new Date(item.date) >= sixMonthsAgo |
|||
); |
|||
} |
|||
|
|||
const chartData = { |
|||
labels: filteredData.map((item) => item.date), |
|||
datasets: [ |
|||
{ |
|||
label: isInvestment ? 'Investment' : 'Portfolio Value', |
|||
data: filteredData.map((item) => |
|||
isInvestment ? item.investment : item.value |
|||
), |
|||
borderColor: isInvestment ? '#388e3c' : '#1976d2', |
|||
backgroundColor: isInvestment |
|||
? 'rgba(56, 142, 60, 0.1)' |
|||
: 'rgba(25, 118, 210, 0.1)', |
|||
borderWidth: 2, |
|||
pointRadius: 0, |
|||
fill: true, |
|||
tension: 0 |
|||
} |
|||
] |
|||
}; |
|||
|
|||
if (!isInvestment) { |
|||
// Add benchmark data |
|||
chartData.datasets.push({ |
|||
label: 'Benchmark (S&P 500)', |
|||
data: filteredData.map( |
|||
(item) => |
|||
(isInvestment ? item.investment : item.value) * |
|||
(0.9 + Math.random() * 0.2) |
|||
), |
|||
borderColor: '#f57c00', |
|||
backgroundColor: 'transparent', |
|||
borderWidth: 2, |
|||
pointRadius: 0, |
|||
fill: false, |
|||
tension: 0 |
|||
}); |
|||
} |
|||
|
|||
return new Chart(ctx, { |
|||
type: 'line', |
|||
data: chartData, |
|||
options: { |
|||
responsive: true, |
|||
maintainAspectRatio: false, |
|||
plugins: { |
|||
title: { |
|||
display: true, |
|||
text: title |
|||
}, |
|||
legend: { |
|||
position: 'top' |
|||
}, |
|||
tooltip: { |
|||
mode: 'index', |
|||
intersect: false |
|||
} |
|||
}, |
|||
scales: { |
|||
x: { |
|||
type: 'time', |
|||
time: { |
|||
unit: 'month' |
|||
}, |
|||
grid: { |
|||
display: false |
|||
} |
|||
}, |
|||
y: { |
|||
beginAtZero: false, |
|||
grid: { |
|||
color: 'rgba(0, 0, 0, 0.05)' |
|||
} |
|||
} |
|||
}, |
|||
interaction: { |
|||
intersect: false, |
|||
mode: 'index' |
|||
} |
|||
} |
|||
}); |
|||
} |
|||
|
|||
// Initialize charts when the page loads |
|||
document.addEventListener('DOMContentLoaded', function () { |
|||
// Generate mock data |
|||
const performanceData = generateMockData(365); |
|||
const investmentData = generateInvestmentData(24); |
|||
const dividendData = generateDividendData(24); |
|||
|
|||
// Create charts |
|||
createChart( |
|||
'chartBefore', |
|||
performanceData, |
|||
'Portfolio Performance (Full Year)' |
|||
); |
|||
createChart( |
|||
'chartAfter', |
|||
performanceData, |
|||
'Portfolio Performance (Last 6 Months)' |
|||
); |
|||
createChart( |
|||
'investmentChart', |
|||
investmentData, |
|||
'Investment Timeline', |
|||
true |
|||
); |
|||
createChart('dividendChart', dividendData, 'Dividend Timeline', true); |
|||
}); |
|||
</script> |
|||
</body> |
|||
</html> |
|||
@ -0,0 +1,273 @@ |
|||
import { |
|||
getChartColorPalette, |
|||
getPerformanceColor, |
|||
getAllocationColor |
|||
} from './chart-theme'; |
|||
import { ColorScheme } from './types'; |
|||
|
|||
/** |
|||
* Standardized Chart Configuration for Ghostfolio |
|||
* |
|||
* Provides consistent styling, layout, and behavior across all chart types |
|||
*/ |
|||
|
|||
export interface ChartConfigOptions { |
|||
colorScheme?: ColorScheme; |
|||
currency?: string; |
|||
locale?: string; |
|||
unit?: string; |
|||
showLegend?: boolean; |
|||
showGrid?: boolean; |
|||
aspectRatio?: number; |
|||
responsive?: boolean; |
|||
maintainAspectRatio?: boolean; |
|||
} |
|||
|
|||
export interface ChartLayoutConfig { |
|||
padding?: number; |
|||
spacing?: number; |
|||
borderRadius?: number; |
|||
borderWidth?: number; |
|||
} |
|||
|
|||
/** |
|||
* Get standardized chart options with consistent styling |
|||
*/ |
|||
export function getStandardChartOptions( |
|||
options: ChartConfigOptions = {}, |
|||
layoutConfig: ChartLayoutConfig = {} |
|||
): any { |
|||
const { |
|||
colorScheme = 'LIGHT', |
|||
showLegend = false, |
|||
showGrid = true, |
|||
aspectRatio = 16 / 9, |
|||
responsive = true, |
|||
maintainAspectRatio = true |
|||
} = options; |
|||
|
|||
const { |
|||
padding = 0, |
|||
spacing = 2, |
|||
borderRadius = 4, |
|||
borderWidth = 1 |
|||
} = layoutConfig; |
|||
|
|||
const palette = getChartColorPalette(colorScheme); |
|||
|
|||
return { |
|||
responsive, |
|||
maintainAspectRatio, |
|||
aspectRatio, |
|||
layout: { |
|||
padding: padding |
|||
}, |
|||
plugins: { |
|||
legend: { |
|||
display: showLegend, |
|||
position: 'bottom', |
|||
align: 'start', |
|||
labels: { |
|||
usePointStyle: true, |
|||
padding: spacing * 4, |
|||
font: { |
|||
size: 12, |
|||
family: 'Inter, sans-serif' |
|||
}, |
|||
color: palette.text.secondary |
|||
} |
|||
}, |
|||
tooltip: { |
|||
enabled: true, |
|||
backgroundColor: palette.background, |
|||
titleColor: palette.text.primary, |
|||
bodyColor: palette.text.primary, |
|||
borderColor: palette.border, |
|||
borderWidth: 1, |
|||
cornerRadius: borderRadius, |
|||
displayColors: true, |
|||
usePointStyle: true, |
|||
padding: spacing * 3, |
|||
font: { |
|||
size: 12, |
|||
family: 'Inter, sans-serif' |
|||
} |
|||
} |
|||
}, |
|||
scales: { |
|||
x: { |
|||
display: true, |
|||
grid: { |
|||
display: showGrid, |
|||
color: palette.grid, |
|||
tickLength: spacing * 2 |
|||
}, |
|||
border: { |
|||
display: true, |
|||
color: palette.border, |
|||
width: borderWidth |
|||
}, |
|||
ticks: { |
|||
display: true, |
|||
color: palette.text.secondary, |
|||
padding: spacing * 2, |
|||
font: { |
|||
size: 11, |
|||
family: 'Inter, sans-serif' |
|||
} |
|||
} |
|||
}, |
|||
y: { |
|||
display: true, |
|||
position: 'right', |
|||
grid: { |
|||
display: showGrid, |
|||
color: palette.grid, |
|||
tickLength: spacing * 2 |
|||
}, |
|||
ticks: { |
|||
display: true, |
|||
color: palette.text.secondary, |
|||
padding: spacing * 2, |
|||
mirror: true, |
|||
z: 1, |
|||
font: { |
|||
size: 11, |
|||
family: 'Inter, sans-serif' |
|||
} |
|||
} |
|||
} |
|||
}, |
|||
elements: { |
|||
point: { |
|||
radius: 0, |
|||
hoverRadius: spacing * 2, |
|||
borderWidth: 0 |
|||
}, |
|||
line: { |
|||
borderWidth: borderWidth * 2, |
|||
tension: 0, |
|||
fill: false |
|||
}, |
|||
bar: { |
|||
borderWidth: 0, |
|||
borderRadius: borderRadius |
|||
}, |
|||
arc: { |
|||
borderWidth: 0, |
|||
borderRadius: borderRadius |
|||
} |
|||
}, |
|||
animation: false, |
|||
interaction: { |
|||
intersect: false, |
|||
mode: 'index' |
|||
} |
|||
}; |
|||
} |
|||
|
|||
/** |
|||
* Get standardized colors for chart datasets |
|||
*/ |
|||
export function getStandardDatasetColors( |
|||
index: number, |
|||
type: 'line' | 'bar' | 'doughnut' | 'treemap', |
|||
colorScheme: ColorScheme = 'LIGHT', |
|||
value?: number |
|||
): { |
|||
backgroundColor: string | string[]; |
|||
borderColor: string | string[]; |
|||
hoverBackgroundColor?: string | string[]; |
|||
hoverBorderColor?: string | string[]; |
|||
} { |
|||
const palette = getChartColorPalette(colorScheme); |
|||
|
|||
switch (type) { |
|||
case 'line': |
|||
return { |
|||
backgroundColor: 'transparent', |
|||
borderColor: index === 0 ? palette.primary : palette.secondary, |
|||
hoverBackgroundColor: palette.hover |
|||
}; |
|||
|
|||
case 'bar': |
|||
return { |
|||
backgroundColor: getAllocationColor(index, colorScheme), |
|||
borderColor: 'transparent', |
|||
hoverBackgroundColor: palette.hover |
|||
}; |
|||
|
|||
case 'doughnut': |
|||
return { |
|||
backgroundColor: getAllocationColor(index, colorScheme), |
|||
borderColor: 'transparent', |
|||
hoverBackgroundColor: palette.hover |
|||
}; |
|||
|
|||
case 'treemap': |
|||
if (value !== undefined) { |
|||
return { |
|||
backgroundColor: getPerformanceColor(value, colorScheme), |
|||
borderColor: 'transparent' |
|||
}; |
|||
} |
|||
return { |
|||
backgroundColor: getAllocationColor(index, colorScheme), |
|||
borderColor: 'transparent' |
|||
}; |
|||
|
|||
default: |
|||
return { |
|||
backgroundColor: palette.primary, |
|||
borderColor: palette.primary |
|||
}; |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Get responsive breakpoints for charts |
|||
*/ |
|||
export function getChartBreakpoints() { |
|||
return { |
|||
mobile: 480, |
|||
tablet: 768, |
|||
desktop: 1024, |
|||
large: 1440 |
|||
}; |
|||
} |
|||
|
|||
/** |
|||
* Get responsive chart configuration |
|||
*/ |
|||
export function getResponsiveChartConfig( |
|||
colorScheme: ColorScheme = 'LIGHT', |
|||
deviceType: 'mobile' | 'tablet' | 'desktop' = 'desktop' |
|||
): Partial<ChartConfigOptions> { |
|||
const baseConfig: ChartConfigOptions = { |
|||
colorScheme, |
|||
showLegend: deviceType !== 'mobile', |
|||
aspectRatio: deviceType === 'mobile' ? 1 : 16 / 9, |
|||
maintainAspectRatio: deviceType !== 'mobile' |
|||
}; |
|||
|
|||
switch (deviceType) { |
|||
case 'mobile': |
|||
return { |
|||
...baseConfig, |
|||
aspectRatio: 1, |
|||
maintainAspectRatio: false, |
|||
showLegend: false, |
|||
showGrid: false |
|||
}; |
|||
|
|||
case 'tablet': |
|||
return { |
|||
...baseConfig, |
|||
aspectRatio: 4 / 3, |
|||
showLegend: true |
|||
}; |
|||
|
|||
default: |
|||
return baseConfig; |
|||
} |
|||
} |
|||
@ -0,0 +1,287 @@ |
|||
import { ColorScheme } from './types'; |
|||
|
|||
/** |
|||
* Chart Color Theme System for Ghostfolio |
|||
* |
|||
* Provides consistent color schemes across all chart types with support for: |
|||
* - Light and dark themes |
|||
* - Semantic color meanings (positive, negative, neutral) |
|||
* - Performance-based color coding |
|||
* - Accessibility-compliant contrast ratios |
|||
*/ |
|||
|
|||
export interface ChartColorPalette { |
|||
// Primary chart colors
|
|||
primary: string; |
|||
secondary: string; |
|||
accent: string; |
|||
|
|||
// Semantic colors
|
|||
positive: string; |
|||
negative: string; |
|||
neutral: string; |
|||
warning: string; |
|||
info: string; |
|||
|
|||
// Performance-based colors (green to red spectrum)
|
|||
performance: { |
|||
excellent: string; |
|||
good: string; |
|||
average: string; |
|||
poor: string; |
|||
terrible: string; |
|||
}; |
|||
|
|||
// Allocation colors (diverse palette for categories)
|
|||
allocation: string[]; |
|||
|
|||
// Background and text colors
|
|||
background: string; |
|||
surface: string; |
|||
text: { |
|||
primary: string; |
|||
secondary: string; |
|||
disabled: string; |
|||
}; |
|||
|
|||
// Interactive states
|
|||
hover: string; |
|||
active: string; |
|||
focus: string; |
|||
|
|||
// Grid and border colors
|
|||
grid: string; |
|||
border: string; |
|||
} |
|||
|
|||
export interface ChartTheme { |
|||
light: ChartColorPalette; |
|||
dark: ChartColorPalette; |
|||
} |
|||
|
|||
/** |
|||
* Generate color palette based on color scheme |
|||
*/ |
|||
export function getChartColorPalette( |
|||
colorScheme: ColorScheme = 'LIGHT' |
|||
): ChartColorPalette { |
|||
const isDark = colorScheme === 'DARK'; |
|||
|
|||
if (isDark) { |
|||
return { |
|||
// Primary chart colors
|
|||
primary: '#64b5f6', // Light blue
|
|||
secondary: '#81c784', // Light green
|
|||
accent: '#ffb74d', // Orange
|
|||
|
|||
// Semantic colors
|
|||
positive: '#4caf50', // Green
|
|||
negative: '#f44336', // Red
|
|||
neutral: '#9e9e9e', // Grey
|
|||
warning: '#ff9800', // Orange
|
|||
info: '#2196f3', // Blue
|
|||
|
|||
// Performance-based colors
|
|||
performance: { |
|||
excellent: '#2e7d32', // Dark green
|
|||
good: '#4caf50', // Green
|
|||
average: '#8bc34a', // Light green
|
|||
poor: '#ff5722', // Deep orange
|
|||
terrible: '#d32f2f' // Dark red
|
|||
}, |
|||
|
|||
// Allocation colors (diverse palette)
|
|||
allocation: [ |
|||
'#2196f3', // Blue
|
|||
'#4caf50', // Green
|
|||
'#ff9800', // Orange
|
|||
'#9c27b0', // Purple
|
|||
'#f44336', // Red
|
|||
'#00bcd4', // Cyan
|
|||
'#ffc107', // Amber
|
|||
'#795548', // Brown
|
|||
'#607d8b', // Blue grey
|
|||
'#e91e63', // Pink
|
|||
'#3f51b5', // Indigo
|
|||
'#009688', // Teal
|
|||
'#8bc34a', // Light green
|
|||
'#ff5722', // Deep orange
|
|||
'#9c27b0' // Purple
|
|||
], |
|||
|
|||
// Background and text colors
|
|||
background: '#121212', |
|||
surface: '#1e1e1e', |
|||
text: { |
|||
primary: '#ffffff', |
|||
secondary: '#b3b3b3', |
|||
disabled: '#666666' |
|||
}, |
|||
|
|||
// Interactive states
|
|||
hover: 'rgba(255, 255, 255, 0.08)', |
|||
active: 'rgba(255, 255, 255, 0.16)', |
|||
focus: 'rgba(100, 181, 246, 0.24)', |
|||
|
|||
// Grid and border colors
|
|||
grid: 'rgba(255, 255, 255, 0.08)', |
|||
border: 'rgba(255, 255, 255, 0.12)' |
|||
}; |
|||
} |
|||
|
|||
// Light theme
|
|||
return { |
|||
// Primary chart colors
|
|||
primary: '#1976d2', // Blue
|
|||
secondary: '#388e3c', // Green
|
|||
accent: '#f57c00', // Orange
|
|||
|
|||
// Semantic colors
|
|||
positive: '#2e7d32', // Green
|
|||
negative: '#d32f2f', // Red
|
|||
neutral: '#757575', // Grey
|
|||
warning: '#f57c00', // Orange
|
|||
info: '#1976d2', // Blue
|
|||
|
|||
// Performance-based colors
|
|||
performance: { |
|||
excellent: '#1b5e20', // Dark green
|
|||
good: '#2e7d32', // Green
|
|||
average: '#4caf50', // Light green
|
|||
poor: '#f57c00', // Orange
|
|||
terrible: '#d32f2f' // Dark red
|
|||
}, |
|||
|
|||
// Allocation colors (diverse palette)
|
|||
allocation: [ |
|||
'#1976d2', // Blue
|
|||
'#388e3c', // Green
|
|||
'#f57c00', // Orange
|
|||
'#7b1fa2', // Purple
|
|||
'#d32f2f', // Red
|
|||
'#00796b', // Teal
|
|||
'#fbc02d', // Yellow
|
|||
'#5d4037', // Brown
|
|||
'#455a64', // Blue grey
|
|||
'#c2185b', // Pink
|
|||
'#3f51b5', // Indigo
|
|||
'#009688', // Teal
|
|||
'#689f38', // Light green
|
|||
'#e65100', // Deep orange
|
|||
'#7b1fa2' // Purple
|
|||
], |
|||
|
|||
// Background and text colors
|
|||
background: '#ffffff', |
|||
surface: '#f5f5f5', |
|||
text: { |
|||
primary: '#212121', |
|||
secondary: '#757575', |
|||
disabled: '#bdbdbd' |
|||
}, |
|||
|
|||
// Interactive states
|
|||
hover: 'rgba(0, 0, 0, 0.04)', |
|||
active: 'rgba(0, 0, 0, 0.08)', |
|||
focus: 'rgba(25, 118, 210, 0.12)', |
|||
|
|||
// Grid and border colors
|
|||
grid: 'rgba(0, 0, 0, 0.08)', |
|||
border: 'rgba(0, 0, 0, 0.12)' |
|||
}; |
|||
} |
|||
|
|||
/** |
|||
* Get performance color based on percentage value |
|||
*/ |
|||
export function getPerformanceColor( |
|||
percentage: number, |
|||
colorScheme: ColorScheme = 'LIGHT' |
|||
): string { |
|||
const palette = getChartColorPalette(colorScheme); |
|||
|
|||
if (percentage >= 15) return palette.performance.excellent; |
|||
if (percentage >= 5) return palette.performance.good; |
|||
if (percentage >= -5) return palette.performance.average; |
|||
if (percentage >= -15) return palette.performance.poor; |
|||
return palette.performance.terrible; |
|||
} |
|||
|
|||
/** |
|||
* Get allocation color for index-based coloring |
|||
*/ |
|||
export function getAllocationColor( |
|||
index: number, |
|||
colorScheme: ColorScheme = 'LIGHT' |
|||
): string { |
|||
const palette = getChartColorPalette(colorScheme); |
|||
return palette.allocation[index % palette.allocation.length]; |
|||
} |
|||
|
|||
/** |
|||
* Get semantic color based on context |
|||
*/ |
|||
export function getSemanticColor( |
|||
type: 'positive' | 'negative' | 'neutral' | 'warning' | 'info', |
|||
colorScheme: ColorScheme = 'LIGHT' |
|||
): string { |
|||
const palette = getChartColorPalette(colorScheme); |
|||
return palette[type]; |
|||
} |
|||
|
|||
/** |
|||
* Calculate relative luminance of a color (0-1 scale) |
|||
* Based on WCAG guidelines for color contrast |
|||
*/ |
|||
function calculateLuminance(color: string): number { |
|||
// Convert hex to RGB
|
|||
const hex = color.replace('#', ''); |
|||
const r = parseInt(hex.substr(0, 2), 16) / 255; |
|||
const g = parseInt(hex.substr(2, 2), 16) / 255; |
|||
const b = parseInt(hex.substr(4, 2), 16) / 255; |
|||
|
|||
// Apply gamma correction
|
|||
const gammaCorrect = (value: number): number => { |
|||
return value <= 0.03928 |
|||
? value / 12.92 |
|||
: Math.pow((value + 0.055) / 1.055, 2.4); |
|||
}; |
|||
|
|||
const rCorrected = gammaCorrect(r); |
|||
const gCorrected = gammaCorrect(g); |
|||
const bCorrected = gammaCorrect(b); |
|||
|
|||
// Calculate relative luminance
|
|||
return 0.2126 * rCorrected + 0.7152 * gCorrected + 0.0722 * bCorrected; |
|||
} |
|||
export function getContrastTextColor( |
|||
backgroundColor: string, |
|||
colorScheme: ColorScheme = 'LIGHT' |
|||
): string { |
|||
const palette = getChartColorPalette(colorScheme); |
|||
|
|||
// Calculate relative luminance of the background color
|
|||
const luminance = calculateLuminance(backgroundColor); |
|||
|
|||
// Use dark text for light backgrounds, light text for dark backgrounds
|
|||
return luminance > 0.5 ? palette.text.primary : '#ffffff'; |
|||
} |
|||
|
|||
/** |
|||
* Generate gradient colors for area charts |
|||
*/ |
|||
export function generateGradientColors( |
|||
baseColor: string, |
|||
colorScheme: ColorScheme = 'LIGHT', |
|||
opacity: number = 0.1 |
|||
): { start: string; end: string } { |
|||
// Using palette for potential future enhancements
|
|||
getChartColorPalette(colorScheme); |
|||
|
|||
return { |
|||
start: `${baseColor}${Math.round(opacity * 255) |
|||
.toString(16) |
|||
.padStart(2, '0')}`,
|
|||
end: baseColor |
|||
}; |
|||
} |
|||
Loading…
Reference in new issue