From e2d03107df7c49dd09e215fcc96df29fe8bb61d4 Mon Sep 17 00:00:00 2001 From: Juergen Kunz Date: Sat, 4 Apr 2026 12:29:39 +0000 Subject: [PATCH] fix(chart): refine ECharts series styling and legend color handling across bar, donut, and radar charts --- changelog.md | 8 +++ ts_web/00_commitinfo_data.ts | 2 +- .../00group-chart/dees-chart-bar/component.ts | 54 ++++++++++++------- .../dees-chart-donut/component.ts | 54 +++++++++++-------- .../00group-chart/dees-chart-donut/styles.ts | 3 ++ .../00group-chart/dees-chart-echarts-base.ts | 7 ++- .../dees-chart-echarts-styles.ts | 1 - .../00group-chart/dees-chart-echarts-theme.ts | 49 +++++++++++------ .../dees-chart-radar/component.ts | 12 +++-- 9 files changed, 126 insertions(+), 64 deletions(-) diff --git a/changelog.md b/changelog.md index 7835ba0..e1397c0 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,13 @@ # Changelog +## 2026-04-04 - 3.55.5 - fix(chart) +refine ECharts series styling and legend color handling across bar, donut, and radar charts + +- switch chart series palettes to hex colors and add rgba conversion to prevent black flashes during ECharts hover and emphasis animations +- explicitly provide legend item colors and solid tooltip markers so translucent fills render consistently across chart types +- deep-merge legend theme options in the shared ECharts base component to preserve nested legend text styling +- adjust donut chart spacing and shared chart container styling for improved layout + ## 2026-04-04 - 3.55.4 - fix(chart) align ECharts components with theme tokens and load the full ECharts ESM bundle diff --git a/ts_web/00_commitinfo_data.ts b/ts_web/00_commitinfo_data.ts index 3577f03..3fd2ea1 100644 --- a/ts_web/00_commitinfo_data.ts +++ b/ts_web/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: '@design.estate/dees-catalog', - version: '3.55.4', + version: '3.55.5', description: 'A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.' } diff --git a/ts_web/elements/00group-chart/dees-chart-bar/component.ts b/ts_web/elements/00group-chart/dees-chart-bar/component.ts index 311e6be..e43a573 100644 --- a/ts_web/elements/00group-chart/dees-chart-bar/component.ts +++ b/ts_web/elements/00group-chart/dees-chart-bar/component.ts @@ -8,7 +8,7 @@ import { DeesChartEchartsBase } from '../dees-chart-echarts-base.js'; import { demoFunc } from './demo.js'; import { barStyles } from './styles.js'; import { renderChartBar } from './template.js'; -import { getEchartsSeriesColors, getThemeColors } from '../dees-chart-echarts-theme.js'; +import { getEchartsSeriesColors, getThemeColors, hexToRgba } from '../dees-chart-echarts-theme.js'; export interface IBarSeriesItem { name: string; @@ -87,28 +87,42 @@ export class DeesChartBar extends DeesChartEchartsBase { splitLine: { lineStyle: { color: colors.borderSubtle } }, }; - const seriesData = this.series.map((s, index) => ({ - name: s.name, - type: 'bar' as const, - data: s.data, - stack: this.stacked ? 'total' : undefined, - itemStyle: { - color: s.color || seriesColors[index % seriesColors.length], - borderRadius: this.stacked ? [0, 0, 0, 0] : this.horizontal ? [0, 4, 4, 0] : [4, 4, 0, 0], - }, - barMaxWidth: 40, - emphasis: { + const fillAlpha = this.goBright ? 0.15 : 0.25; + const borderRadius = this.horizontal ? [0, 4, 4, 0] : [4, 4, 0, 0]; + const noBorderRadius = [0, 0, 0, 0]; + + const legendData: Array<{ name: string; itemStyle: { color: string } }> = []; + + const seriesData = this.series.map((s, index) => { + const color = s.color || seriesColors[index % seriesColors.length]; + legendData.push({ name: s.name, itemStyle: { color } }); + return { + name: s.name, + type: 'bar' as const, + data: s.data, + stack: this.stacked ? 'total' : undefined, itemStyle: { - shadowBlur: 6, - shadowColor: 'rgba(0, 0, 0, 0.15)', + color: hexToRgba(color, fillAlpha), + borderColor: color, + borderWidth: 1, + borderRadius: this.stacked ? noBorderRadius : borderRadius, }, - }, - })); + barMaxWidth: 40, + barGap: '20%', + emphasis: { + itemStyle: { + color: hexToRgba(color, fillAlpha + 0.15), + borderColor: color, + borderWidth: 1.5, + }, + }, + }; + }); // For stacked bars, round the top corners of the last visible series if (this.stacked && seriesData.length > 0) { const last = seriesData[seriesData.length - 1]; - last.itemStyle.borderRadius = this.horizontal ? [0, 4, 4, 0] : [4, 4, 0, 0]; + last.itemStyle.borderRadius = borderRadius; } return { @@ -119,13 +133,15 @@ export class DeesChartBar extends DeesChartEchartsBase { const items = Array.isArray(params) ? params : [params]; let result = `${items[0].axisValueLabel}
`; for (const p of items) { - result += `${p.marker} ${p.seriesName}: ${formatter(p.value)}
`; + const solidColor = p.borderColor || p.color; + const marker = ``; + result += `${marker}${p.seriesName}: ${formatter(p.value)}
`; } return result; }, }, legend: this.showLegend && this.series.length > 1 - ? { bottom: 8, itemWidth: 10, itemHeight: 10 } + ? { bottom: 8, itemWidth: 10, itemHeight: 10, data: legendData } : { show: false }, grid: { left: 16, diff --git a/ts_web/elements/00group-chart/dees-chart-donut/component.ts b/ts_web/elements/00group-chart/dees-chart-donut/component.ts index 5609b46..57c9cdb 100644 --- a/ts_web/elements/00group-chart/dees-chart-donut/component.ts +++ b/ts_web/elements/00group-chart/dees-chart-donut/component.ts @@ -8,7 +8,7 @@ import { DeesChartEchartsBase } from '../dees-chart-echarts-base.js'; import { demoFunc } from './demo.js'; import { donutStyles } from './styles.js'; import { renderChartDonut } from './template.js'; -import { getEchartsSeriesColors, getThemeColors } from '../dees-chart-echarts-theme.js'; +import { getEchartsSeriesColors, getThemeColors, hexToRgba } from '../dees-chart-echarts-theme.js'; export interface IDonutDataItem { name: string; @@ -62,13 +62,32 @@ export class DeesChartDonut extends DeesChartEchartsBase { } protected buildOption(): Record { - const colors = getThemeColors(this.goBright); + const themeColors = getThemeColors(this.goBright); const seriesColors = getEchartsSeriesColors(this.goBright); - const data = this.data.map((item, index) => ({ - name: item.name, - value: item.value, - itemStyle: item.color ? { color: item.color } : { color: seriesColors[index % seriesColors.length] }, - })); + const fillAlpha = this.goBright ? 0.15 : 0.2; + + const legendData: Array<{ name: string; itemStyle: { color: string } }> = []; + + const data = this.data.map((item, index) => { + const color = item.color || seriesColors[index % seriesColors.length]; + legendData.push({ name: item.name, itemStyle: { color } }); + return { + name: item.name, + value: item.value, + itemStyle: { + color: hexToRgba(color, fillAlpha), + borderColor: color, + borderWidth: 1, + }, + emphasis: { + itemStyle: { + color: hexToRgba(color, fillAlpha + 0.15), + borderColor: color, + borderWidth: 1.5, + }, + }, + }; + }); const formatter = this.valueFormatter; @@ -76,7 +95,9 @@ export class DeesChartDonut extends DeesChartEchartsBase { tooltip: { trigger: 'item', formatter: (params: any) => { - return `${params.marker} ${params.name}: ${formatter(params.value)} (${params.percent}%)`; + const solidColor = params.data?.itemStyle?.borderColor || params.color; + const marker = ``; + return `${marker}${params.name}: ${formatter(params.value)} (${params.percent}%)`; }, }, legend: this.showLegend @@ -87,6 +108,7 @@ export class DeesChartDonut extends DeesChartEchartsBase { itemWidth: 10, itemHeight: 10, itemGap: 12, + data: legendData, formatter: (name: string) => { const item = this.data.find((d) => d.name === name); return item ? `${name} ${formatter(item.value)}` : name; @@ -99,31 +121,19 @@ export class DeesChartDonut extends DeesChartEchartsBase { radius: [this.innerRadiusPercent, '85%'], center: this.showLegend ? ['35%', '50%'] : ['50%', '50%'], avoidLabelOverlap: true, + padAngle: 2, itemStyle: { borderRadius: 4, - borderColor: 'transparent', - borderWidth: 2, }, label: this.showLabels ? { show: true, formatter: '{b}: {d}%', fontSize: 11, - color: colors.textSecondary, + color: themeColors.textSecondary, textBorderColor: 'transparent', } : { show: false }, - emphasis: { - itemStyle: { - shadowBlur: 10, - shadowOffsetX: 0, - shadowColor: 'rgba(0, 0, 0, 0.2)', - }, - label: { - show: true, - fontWeight: 'bold', - }, - }, data, }, ], diff --git a/ts_web/elements/00group-chart/dees-chart-donut/styles.ts b/ts_web/elements/00group-chart/dees-chart-donut/styles.ts index ca4fa80..b4b2425 100644 --- a/ts_web/elements/00group-chart/dees-chart-donut/styles.ts +++ b/ts_web/elements/00group-chart/dees-chart-donut/styles.ts @@ -7,5 +7,8 @@ export const donutStyles = [ :host { height: 360px; } + .chartContainer { + inset: 12px 0; + } `, ]; diff --git a/ts_web/elements/00group-chart/dees-chart-echarts-base.ts b/ts_web/elements/00group-chart/dees-chart-echarts-base.ts index 739290c..8c707f3 100644 --- a/ts_web/elements/00group-chart/dees-chart-echarts-base.ts +++ b/ts_web/elements/00group-chart/dees-chart-echarts-base.ts @@ -87,12 +87,17 @@ export abstract class DeesChartEchartsBase extends DeesElement { if (!this.chartInstance) return; const themeOptions = getEchartsThemeOptions(this.goBright); const chartOption = this.buildOption(); - // Merge theme defaults with chart-specific options + // Deep-merge theme defaults with chart-specific options for nested objects const merged = { ...themeOptions, ...chartOption, textStyle: { ...themeOptions.textStyle, ...(chartOption.textStyle || {}) }, tooltip: { ...themeOptions.tooltip, ...(chartOption.tooltip || {}) }, + legend: { + ...themeOptions.legend, + ...(chartOption.legend || {}), + textStyle: { ...(themeOptions.legend?.textStyle || {}), ...(chartOption.legend?.textStyle || {}) }, + }, }; this.chartInstance.setOption(merged, true); } diff --git a/ts_web/elements/00group-chart/dees-chart-echarts-styles.ts b/ts_web/elements/00group-chart/dees-chart-echarts-styles.ts index 3487c6c..ff9579f 100644 --- a/ts_web/elements/00group-chart/dees-chart-echarts-styles.ts +++ b/ts_web/elements/00group-chart/dees-chart-echarts-styles.ts @@ -31,7 +31,6 @@ export const echartsBaseStyles = [ .chartContainer { position: absolute; inset: 0; - will-change: transform; } `, ]; diff --git a/ts_web/elements/00group-chart/dees-chart-echarts-theme.ts b/ts_web/elements/00group-chart/dees-chart-echarts-theme.ts index 5f8a72d..0273014 100644 --- a/ts_web/elements/00group-chart/dees-chart-echarts-theme.ts +++ b/ts_web/elements/00group-chart/dees-chart-echarts-theme.ts @@ -3,8 +3,12 @@ * Uses the centralized themeDefaults tokens so chart colors stay in sync * with the rest of the dees-catalog design system. * - * ECharts renders on and cannot read CSS custom properties, + * ECharts renders on and cannot read CSS custom properties, * so we reference the TypeScript source-of-truth (themeDefaults) directly. + * + * IMPORTANT: All colors passed to ECharts for data series must be hex or rgb/rgba. + * ECharts cannot interpolate HSL strings during hover/emphasis animations, + * causing them to flash black. */ import { themeDefaults } from '../00theme.js'; @@ -12,22 +16,27 @@ import { themeDefaults } from '../00theme.js'; const light = themeDefaults.colors.light; const dark = themeDefaults.colors.dark; +/** + * Series color palette for ECharts charts. + * Aligned with the Tailwind/shadcn-inspired palette used throughout the codebase. + * All values are hex — ECharts requires this for animation interpolation. + */ const SERIES_COLORS = { dark: [ - dark.accentPrimary, // blue - 'hsl(173.4 80.4% 40%)', // teal (no token yet) - 'hsl(280.3 87.4% 66.7%)', // purple (no token yet) - dark.accentWarning, // orange/amber - dark.accentSuccess, // green - dark.accentError, // rose/red + '#60a5fa', // blue-400 — softer in dark mode + '#2dd4bf', // teal-400 + '#a78bfa', // violet-400 + '#fbbf24', // amber-400 + '#34d399', // emerald-400 + '#fb7185', // rose-400 ], light: [ - light.accentPrimary, - 'hsl(142.1 76.2% 36.3%)', // teal (no token yet) - 'hsl(280.3 47.7% 50.2%)', // purple (no token yet) - light.accentWarning, - light.accentSuccess, - light.accentError, + '#3b82f6', // blue-500 + '#14b8a6', // teal-500 + '#8b5cf6', // violet-500 + '#f59e0b', // amber-500 + '#10b981', // emerald-500 + '#f43f5e', // rose-500 ], }; @@ -35,6 +44,16 @@ export function getEchartsSeriesColors(goBright: boolean): string[] { return goBright ? SERIES_COLORS.light : SERIES_COLORS.dark; } +/** + * Convert a hex color to an rgba string with the given alpha. + */ +export function hexToRgba(hex: string, alpha: number): string { + const r = parseInt(hex.slice(1, 3), 16); + const g = parseInt(hex.slice(3, 5), 16); + const b = parseInt(hex.slice(5, 7), 16); + return `rgba(${r}, ${g}, ${b}, ${alpha})`; +} + export function getEchartsThemeOptions(goBright: boolean): Record { const colors = goBright ? light : dark; return { @@ -44,7 +63,8 @@ export function getEchartsThemeOptions(goBright: boolean): Record { fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif', fontSize: 12, }, - color: goBright ? SERIES_COLORS.light : SERIES_COLORS.dark, + // No global `color` array — each component sets per-item/per-series + // colors explicitly to avoid conflicts during emphasis animations. tooltip: { backgroundColor: colors.bgPrimary, borderColor: colors.borderDefault, @@ -65,7 +85,6 @@ export function getEchartsThemeOptions(goBright: boolean): Record { /** * Helper to get the resolved theme colors object for use in buildOption(). - * Components can use this instead of hardcoding dark/light color values. */ export function getThemeColors(goBright: boolean) { return goBright ? light : dark; diff --git a/ts_web/elements/00group-chart/dees-chart-radar/component.ts b/ts_web/elements/00group-chart/dees-chart-radar/component.ts index d100094..211780e 100644 --- a/ts_web/elements/00group-chart/dees-chart-radar/component.ts +++ b/ts_web/elements/00group-chart/dees-chart-radar/component.ts @@ -8,7 +8,7 @@ import { DeesChartEchartsBase } from '../dees-chart-echarts-base.js'; import { demoFunc } from './demo.js'; import { radarStyles } from './styles.js'; import { renderChartRadar } from './template.js'; -import { getEchartsSeriesColors, getThemeColors } from '../dees-chart-echarts-theme.js'; +import { getEchartsSeriesColors, getThemeColors, hexToRgba } from '../dees-chart-echarts-theme.js'; export interface IRadarIndicator { name: string; @@ -67,16 +67,18 @@ export class DeesChartRadar extends DeesChartEchartsBase { const colors = getThemeColors(this.goBright); const seriesColors = getEchartsSeriesColors(this.goBright); + const fillAlpha = this.goBright ? 0.1 : 0.15; + const seriesData = this.series.map((s, index) => { const color = s.color || seriesColors[index % seriesColors.length]; return { name: s.name, value: s.values, - itemStyle: { color }, - lineStyle: { color, width: 2 }, - areaStyle: this.fillArea ? { color, opacity: 0.15 } : undefined, + itemStyle: { color, borderColor: color, borderWidth: 1 }, + lineStyle: { color, width: 1.5 }, + areaStyle: this.fillArea ? { color: hexToRgba(color, fillAlpha) } : undefined, symbol: 'circle', - symbolSize: 6, + symbolSize: 5, }; });