fix(chart): refine ECharts series styling and legend color handling across bar, donut, and radar charts
This commit is contained in:
@@ -1,5 +1,13 @@
|
|||||||
# Changelog
|
# 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)
|
## 2026-04-04 - 3.55.4 - fix(chart)
|
||||||
align ECharts components with theme tokens and load the full ECharts ESM bundle
|
align ECharts components with theme tokens and load the full ECharts ESM bundle
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@design.estate/dees-catalog',
|
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.'
|
description: 'A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { DeesChartEchartsBase } from '../dees-chart-echarts-base.js';
|
|||||||
import { demoFunc } from './demo.js';
|
import { demoFunc } from './demo.js';
|
||||||
import { barStyles } from './styles.js';
|
import { barStyles } from './styles.js';
|
||||||
import { renderChartBar } from './template.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 {
|
export interface IBarSeriesItem {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -87,28 +87,42 @@ export class DeesChartBar extends DeesChartEchartsBase {
|
|||||||
splitLine: { lineStyle: { color: colors.borderSubtle } },
|
splitLine: { lineStyle: { color: colors.borderSubtle } },
|
||||||
};
|
};
|
||||||
|
|
||||||
const seriesData = this.series.map((s, index) => ({
|
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,
|
name: s.name,
|
||||||
type: 'bar' as const,
|
type: 'bar' as const,
|
||||||
data: s.data,
|
data: s.data,
|
||||||
stack: this.stacked ? 'total' : undefined,
|
stack: this.stacked ? 'total' : undefined,
|
||||||
itemStyle: {
|
itemStyle: {
|
||||||
color: s.color || seriesColors[index % seriesColors.length],
|
color: hexToRgba(color, fillAlpha),
|
||||||
borderRadius: this.stacked ? [0, 0, 0, 0] : this.horizontal ? [0, 4, 4, 0] : [4, 4, 0, 0],
|
borderColor: color,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderRadius: this.stacked ? noBorderRadius : borderRadius,
|
||||||
},
|
},
|
||||||
barMaxWidth: 40,
|
barMaxWidth: 40,
|
||||||
|
barGap: '20%',
|
||||||
emphasis: {
|
emphasis: {
|
||||||
itemStyle: {
|
itemStyle: {
|
||||||
shadowBlur: 6,
|
color: hexToRgba(color, fillAlpha + 0.15),
|
||||||
shadowColor: 'rgba(0, 0, 0, 0.15)',
|
borderColor: color,
|
||||||
|
borderWidth: 1.5,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}));
|
};
|
||||||
|
});
|
||||||
|
|
||||||
// For stacked bars, round the top corners of the last visible series
|
// For stacked bars, round the top corners of the last visible series
|
||||||
if (this.stacked && seriesData.length > 0) {
|
if (this.stacked && seriesData.length > 0) {
|
||||||
const last = seriesData[seriesData.length - 1];
|
const last = seriesData[seriesData.length - 1];
|
||||||
last.itemStyle.borderRadius = this.horizontal ? [0, 4, 4, 0] : [4, 4, 0, 0];
|
last.itemStyle.borderRadius = borderRadius;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -119,13 +133,15 @@ export class DeesChartBar extends DeesChartEchartsBase {
|
|||||||
const items = Array.isArray(params) ? params : [params];
|
const items = Array.isArray(params) ? params : [params];
|
||||||
let result = `<strong>${items[0].axisValueLabel}</strong><br/>`;
|
let result = `<strong>${items[0].axisValueLabel}</strong><br/>`;
|
||||||
for (const p of items) {
|
for (const p of items) {
|
||||||
result += `${p.marker} ${p.seriesName}: <strong>${formatter(p.value)}</strong><br/>`;
|
const solidColor = p.borderColor || p.color;
|
||||||
|
const marker = `<span style="display:inline-block;margin-right:4px;border-radius:10px;width:10px;height:10px;background-color:${solidColor};"></span>`;
|
||||||
|
result += `${marker}${p.seriesName}: <strong>${formatter(p.value)}</strong><br/>`;
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
legend: this.showLegend && this.series.length > 1
|
legend: this.showLegend && this.series.length > 1
|
||||||
? { bottom: 8, itemWidth: 10, itemHeight: 10 }
|
? { bottom: 8, itemWidth: 10, itemHeight: 10, data: legendData }
|
||||||
: { show: false },
|
: { show: false },
|
||||||
grid: {
|
grid: {
|
||||||
left: 16,
|
left: 16,
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { DeesChartEchartsBase } from '../dees-chart-echarts-base.js';
|
|||||||
import { demoFunc } from './demo.js';
|
import { demoFunc } from './demo.js';
|
||||||
import { donutStyles } from './styles.js';
|
import { donutStyles } from './styles.js';
|
||||||
import { renderChartDonut } from './template.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 {
|
export interface IDonutDataItem {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -62,13 +62,32 @@ export class DeesChartDonut extends DeesChartEchartsBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected buildOption(): Record<string, any> {
|
protected buildOption(): Record<string, any> {
|
||||||
const colors = getThemeColors(this.goBright);
|
const themeColors = getThemeColors(this.goBright);
|
||||||
const seriesColors = getEchartsSeriesColors(this.goBright);
|
const seriesColors = getEchartsSeriesColors(this.goBright);
|
||||||
const data = this.data.map((item, index) => ({
|
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,
|
name: item.name,
|
||||||
value: item.value,
|
value: item.value,
|
||||||
itemStyle: item.color ? { color: item.color } : { color: seriesColors[index % seriesColors.length] },
|
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;
|
const formatter = this.valueFormatter;
|
||||||
|
|
||||||
@@ -76,7 +95,9 @@ export class DeesChartDonut extends DeesChartEchartsBase {
|
|||||||
tooltip: {
|
tooltip: {
|
||||||
trigger: 'item',
|
trigger: 'item',
|
||||||
formatter: (params: any) => {
|
formatter: (params: any) => {
|
||||||
return `${params.marker} ${params.name}: <strong>${formatter(params.value)}</strong> (${params.percent}%)`;
|
const solidColor = params.data?.itemStyle?.borderColor || params.color;
|
||||||
|
const marker = `<span style="display:inline-block;margin-right:4px;border-radius:10px;width:10px;height:10px;background-color:${solidColor};"></span>`;
|
||||||
|
return `${marker}${params.name}: <strong>${formatter(params.value)}</strong> (${params.percent}%)`;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
legend: this.showLegend
|
legend: this.showLegend
|
||||||
@@ -87,6 +108,7 @@ export class DeesChartDonut extends DeesChartEchartsBase {
|
|||||||
itemWidth: 10,
|
itemWidth: 10,
|
||||||
itemHeight: 10,
|
itemHeight: 10,
|
||||||
itemGap: 12,
|
itemGap: 12,
|
||||||
|
data: legendData,
|
||||||
formatter: (name: string) => {
|
formatter: (name: string) => {
|
||||||
const item = this.data.find((d) => d.name === name);
|
const item = this.data.find((d) => d.name === name);
|
||||||
return item ? `${name} ${formatter(item.value)}` : name;
|
return item ? `${name} ${formatter(item.value)}` : name;
|
||||||
@@ -99,31 +121,19 @@ export class DeesChartDonut extends DeesChartEchartsBase {
|
|||||||
radius: [this.innerRadiusPercent, '85%'],
|
radius: [this.innerRadiusPercent, '85%'],
|
||||||
center: this.showLegend ? ['35%', '50%'] : ['50%', '50%'],
|
center: this.showLegend ? ['35%', '50%'] : ['50%', '50%'],
|
||||||
avoidLabelOverlap: true,
|
avoidLabelOverlap: true,
|
||||||
|
padAngle: 2,
|
||||||
itemStyle: {
|
itemStyle: {
|
||||||
borderRadius: 4,
|
borderRadius: 4,
|
||||||
borderColor: 'transparent',
|
|
||||||
borderWidth: 2,
|
|
||||||
},
|
},
|
||||||
label: this.showLabels
|
label: this.showLabels
|
||||||
? {
|
? {
|
||||||
show: true,
|
show: true,
|
||||||
formatter: '{b}: {d}%',
|
formatter: '{b}: {d}%',
|
||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
color: colors.textSecondary,
|
color: themeColors.textSecondary,
|
||||||
textBorderColor: 'transparent',
|
textBorderColor: 'transparent',
|
||||||
}
|
}
|
||||||
: { show: false },
|
: { show: false },
|
||||||
emphasis: {
|
|
||||||
itemStyle: {
|
|
||||||
shadowBlur: 10,
|
|
||||||
shadowOffsetX: 0,
|
|
||||||
shadowColor: 'rgba(0, 0, 0, 0.2)',
|
|
||||||
},
|
|
||||||
label: {
|
|
||||||
show: true,
|
|
||||||
fontWeight: 'bold',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
data,
|
data,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -7,5 +7,8 @@ export const donutStyles = [
|
|||||||
:host {
|
:host {
|
||||||
height: 360px;
|
height: 360px;
|
||||||
}
|
}
|
||||||
|
.chartContainer {
|
||||||
|
inset: 12px 0;
|
||||||
|
}
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -87,12 +87,17 @@ export abstract class DeesChartEchartsBase extends DeesElement {
|
|||||||
if (!this.chartInstance) return;
|
if (!this.chartInstance) return;
|
||||||
const themeOptions = getEchartsThemeOptions(this.goBright);
|
const themeOptions = getEchartsThemeOptions(this.goBright);
|
||||||
const chartOption = this.buildOption();
|
const chartOption = this.buildOption();
|
||||||
// Merge theme defaults with chart-specific options
|
// Deep-merge theme defaults with chart-specific options for nested objects
|
||||||
const merged = {
|
const merged = {
|
||||||
...themeOptions,
|
...themeOptions,
|
||||||
...chartOption,
|
...chartOption,
|
||||||
textStyle: { ...themeOptions.textStyle, ...(chartOption.textStyle || {}) },
|
textStyle: { ...themeOptions.textStyle, ...(chartOption.textStyle || {}) },
|
||||||
tooltip: { ...themeOptions.tooltip, ...(chartOption.tooltip || {}) },
|
tooltip: { ...themeOptions.tooltip, ...(chartOption.tooltip || {}) },
|
||||||
|
legend: {
|
||||||
|
...themeOptions.legend,
|
||||||
|
...(chartOption.legend || {}),
|
||||||
|
textStyle: { ...(themeOptions.legend?.textStyle || {}), ...(chartOption.legend?.textStyle || {}) },
|
||||||
|
},
|
||||||
};
|
};
|
||||||
this.chartInstance.setOption(merged, true);
|
this.chartInstance.setOption(merged, true);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ export const echartsBaseStyles = [
|
|||||||
.chartContainer {
|
.chartContainer {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
will-change: transform;
|
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -3,8 +3,12 @@
|
|||||||
* Uses the centralized themeDefaults tokens so chart colors stay in sync
|
* Uses the centralized themeDefaults tokens so chart colors stay in sync
|
||||||
* with the rest of the dees-catalog design system.
|
* with the rest of the dees-catalog design system.
|
||||||
*
|
*
|
||||||
* ECharts renders on <canvas> and cannot read CSS custom properties,
|
* ECharts renders on <svg> and cannot read CSS custom properties,
|
||||||
* so we reference the TypeScript source-of-truth (themeDefaults) directly.
|
* 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';
|
import { themeDefaults } from '../00theme.js';
|
||||||
@@ -12,22 +16,27 @@ import { themeDefaults } from '../00theme.js';
|
|||||||
const light = themeDefaults.colors.light;
|
const light = themeDefaults.colors.light;
|
||||||
const dark = themeDefaults.colors.dark;
|
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 = {
|
const SERIES_COLORS = {
|
||||||
dark: [
|
dark: [
|
||||||
dark.accentPrimary, // blue
|
'#60a5fa', // blue-400 — softer in dark mode
|
||||||
'hsl(173.4 80.4% 40%)', // teal (no token yet)
|
'#2dd4bf', // teal-400
|
||||||
'hsl(280.3 87.4% 66.7%)', // purple (no token yet)
|
'#a78bfa', // violet-400
|
||||||
dark.accentWarning, // orange/amber
|
'#fbbf24', // amber-400
|
||||||
dark.accentSuccess, // green
|
'#34d399', // emerald-400
|
||||||
dark.accentError, // rose/red
|
'#fb7185', // rose-400
|
||||||
],
|
],
|
||||||
light: [
|
light: [
|
||||||
light.accentPrimary,
|
'#3b82f6', // blue-500
|
||||||
'hsl(142.1 76.2% 36.3%)', // teal (no token yet)
|
'#14b8a6', // teal-500
|
||||||
'hsl(280.3 47.7% 50.2%)', // purple (no token yet)
|
'#8b5cf6', // violet-500
|
||||||
light.accentWarning,
|
'#f59e0b', // amber-500
|
||||||
light.accentSuccess,
|
'#10b981', // emerald-500
|
||||||
light.accentError,
|
'#f43f5e', // rose-500
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -35,6 +44,16 @@ export function getEchartsSeriesColors(goBright: boolean): string[] {
|
|||||||
return goBright ? SERIES_COLORS.light : SERIES_COLORS.dark;
|
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<string, any> {
|
export function getEchartsThemeOptions(goBright: boolean): Record<string, any> {
|
||||||
const colors = goBright ? light : dark;
|
const colors = goBright ? light : dark;
|
||||||
return {
|
return {
|
||||||
@@ -44,7 +63,8 @@ export function getEchartsThemeOptions(goBright: boolean): Record<string, any> {
|
|||||||
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif',
|
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif',
|
||||||
fontSize: 12,
|
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: {
|
tooltip: {
|
||||||
backgroundColor: colors.bgPrimary,
|
backgroundColor: colors.bgPrimary,
|
||||||
borderColor: colors.borderDefault,
|
borderColor: colors.borderDefault,
|
||||||
@@ -65,7 +85,6 @@ export function getEchartsThemeOptions(goBright: boolean): Record<string, any> {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper to get the resolved theme colors object for use in buildOption().
|
* 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) {
|
export function getThemeColors(goBright: boolean) {
|
||||||
return goBright ? light : dark;
|
return goBright ? light : dark;
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { DeesChartEchartsBase } from '../dees-chart-echarts-base.js';
|
|||||||
import { demoFunc } from './demo.js';
|
import { demoFunc } from './demo.js';
|
||||||
import { radarStyles } from './styles.js';
|
import { radarStyles } from './styles.js';
|
||||||
import { renderChartRadar } from './template.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 {
|
export interface IRadarIndicator {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -67,16 +67,18 @@ export class DeesChartRadar extends DeesChartEchartsBase {
|
|||||||
const colors = getThemeColors(this.goBright);
|
const colors = getThemeColors(this.goBright);
|
||||||
const seriesColors = getEchartsSeriesColors(this.goBright);
|
const seriesColors = getEchartsSeriesColors(this.goBright);
|
||||||
|
|
||||||
|
const fillAlpha = this.goBright ? 0.1 : 0.15;
|
||||||
|
|
||||||
const seriesData = this.series.map((s, index) => {
|
const seriesData = this.series.map((s, index) => {
|
||||||
const color = s.color || seriesColors[index % seriesColors.length];
|
const color = s.color || seriesColors[index % seriesColors.length];
|
||||||
return {
|
return {
|
||||||
name: s.name,
|
name: s.name,
|
||||||
value: s.values,
|
value: s.values,
|
||||||
itemStyle: { color },
|
itemStyle: { color, borderColor: color, borderWidth: 1 },
|
||||||
lineStyle: { color, width: 2 },
|
lineStyle: { color, width: 1.5 },
|
||||||
areaStyle: this.fillArea ? { color, opacity: 0.15 } : undefined,
|
areaStyle: this.fillArea ? { color: hexToRgba(color, fillAlpha) } : undefined,
|
||||||
symbol: 'circle',
|
symbol: 'circle',
|
||||||
symbolSize: 6,
|
symbolSize: 5,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user