Compare commits

...

7 Commits

Author SHA1 Message Date
2104b3bdce v3.55.5
Some checks failed
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-04-04 12:29:39 +00:00
e2d03107df fix(chart): refine ECharts series styling and legend color handling across bar, donut, and radar charts 2026-04-04 12:29:39 +00:00
54a87a5cc0 fix(chart): initialize chart instance with SVG renderer and update styles for chart container 2026-04-04 11:38:18 +00:00
9bfbfcbb95 v3.55.4
Some checks failed
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-04-04 11:25:19 +00:00
3505c390d8 fix(chart): align ECharts components with theme tokens and load the full ECharts ESM bundle 2026-04-04 11:25:19 +00:00
ff32470d8a update 2026-04-04 11:05:01 +00:00
4dba14060e fix(dees-statsgrid): update styles to use shared theme CSS variables 2026-04-04 10:53:53 +00:00
32 changed files with 1539 additions and 20 deletions

View File

@@ -1,5 +1,20 @@
# 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
- replace hardcoded chart colors with centralized themeDefaults-based ECharts theme helpers across bar, donut, gauge, and radar components
- improve donut label styling to use theme-aware text colors
- switch CDN loading to the pre-built echarts.esm.min.js bundle so all chart types and components are available
## 2026-04-04 - 3.55.3 - fix(theme)
align component styles with shared theme CSS variables

View File

@@ -1,6 +1,6 @@
{
"name": "@design.estate/dees-catalog",
"version": "3.55.3",
"version": "3.55.5",
"private": false,
"description": "A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.",
"main": "dist_ts_web/index.js",
@@ -34,6 +34,7 @@
"@tiptap/extension-underline": "^2.23.0",
"@tiptap/starter-kit": "^2.23.0",
"@tsclass/tsclass": "^9.5.0",
"echarts": "^5.6.0",
"lightweight-charts": "^5.1.0",
"highlight.js": "11.11.1",
"ibantools": "^4.5.1",

23
pnpm-lock.yaml generated
View File

@@ -62,6 +62,9 @@ importers:
'@tsclass/tsclass':
specifier: ^9.5.0
version: 9.5.0
echarts:
specifier: ^5.6.0
version: 5.6.0
highlight.js:
specifier: 11.11.1
version: 11.11.1
@@ -2534,6 +2537,9 @@ packages:
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
engines: {node: '>= 0.4'}
echarts@5.6.0:
resolution: {integrity: sha512-oTbVTsXfKuEhxftHqL5xprgLoc0k7uScAwtryCgWF6hPYFLRwOUHiFmHGCBKP5NPFNkDVopOieyUqYGH8Fa3kA==}
emoji-regex@8.0.0:
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
@@ -3936,6 +3942,9 @@ packages:
tslib@1.14.1:
resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==}
tslib@2.3.0:
resolution: {integrity: sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==}
tslib@2.8.1:
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
@@ -4149,6 +4158,9 @@ packages:
zod@3.25.76:
resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==}
zrender@5.6.1:
resolution: {integrity: sha512-OFXkDJKcrlx5su2XbzJvj/34Q3m6PvyCZkVPHGYpcCJ52ek4U/ymZyfuV1nKE23AyBJ51E/6Yr0mhZ7xGTO4ag==}
zwitch@2.0.4:
resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==}
@@ -7671,6 +7683,11 @@ snapshots:
es-errors: 1.3.0
gopd: 1.2.0
echarts@5.6.0:
dependencies:
tslib: 2.3.0
zrender: 5.6.1
emoji-regex@8.0.0: {}
end-of-stream@1.4.5:
@@ -9510,6 +9527,8 @@ snapshots:
tslib@1.14.1: {}
tslib@2.3.0: {}
tslib@2.8.1: {}
tsx@4.21.0:
@@ -9699,4 +9718,8 @@ snapshots:
zod@3.25.76: {}
zrender@5.6.1:
dependencies:
tslib: 2.3.0
zwitch@2.0.4: {}

View File

@@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@design.estate/dees-catalog',
version: '3.55.3',
version: '3.55.5',
description: 'A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.'
}

View File

@@ -0,0 +1,158 @@
import {
customElement,
property,
type TemplateResult,
} from '@design.estate/dees-element';
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, hexToRgba } from '../dees-chart-echarts-theme.js';
export interface IBarSeriesItem {
name: string;
data: number[];
color?: string;
}
declare global {
interface HTMLElementTagNameMap {
'dees-chart-bar': DeesChartBar;
}
}
@customElement('dees-chart-bar')
export class DeesChartBar extends DeesChartEchartsBase {
public static demo = demoFunc;
public static demoGroups = ['Chart'];
@property({ type: Array })
accessor categories: string[] = [];
@property({ type: Array })
accessor series: IBarSeriesItem[] = [];
@property({ type: Boolean })
accessor horizontal: boolean = false;
@property({ type: Boolean })
accessor stacked: boolean = false;
@property({ type: Boolean })
accessor showLegend: boolean = true;
@property({ attribute: false })
accessor valueFormatter: (value: number) => string = (val) => `${val}`;
public static styles = barStyles;
public render(): TemplateResult {
return renderChartBar(this);
}
public async updated(changedProperties: Map<string, any>) {
super.updated(changedProperties);
if (
this.chartInstance &&
(changedProperties.has('categories') ||
changedProperties.has('series') ||
changedProperties.has('horizontal') ||
changedProperties.has('stacked') ||
changedProperties.has('showLegend'))
) {
this.updateChart();
}
}
protected buildOption(): Record<string, any> {
const colors = getThemeColors(this.goBright);
const seriesColors = getEchartsSeriesColors(this.goBright);
const formatter = this.valueFormatter;
const categoryAxis: Record<string, any> = {
type: 'category',
data: this.categories,
axisLine: { lineStyle: { color: colors.borderStrong } },
axisLabel: { color: colors.textMuted },
};
const valueAxis: Record<string, any> = {
type: 'value',
axisLine: { show: false },
axisLabel: {
color: colors.textMuted,
formatter: (val: number) => formatter(val),
},
splitLine: { lineStyle: { color: colors.borderSubtle } },
};
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: {
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 = borderRadius;
}
return {
tooltip: {
trigger: 'axis',
axisPointer: { type: 'shadow' },
formatter: (params: any) => {
const items = Array.isArray(params) ? params : [params];
let result = `<strong>${items[0].axisValueLabel}</strong><br/>`;
for (const p of items) {
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;
},
},
legend: this.showLegend && this.series.length > 1
? { bottom: 8, itemWidth: 10, itemHeight: 10, data: legendData }
: { show: false },
grid: {
left: 16,
right: 16,
top: 16,
bottom: this.showLegend && this.series.length > 1 ? 40 : 16,
containLabel: true,
},
xAxis: this.horizontal ? valueAxis : categoryAxis,
yAxis: this.horizontal ? categoryAxis : valueAxis,
series: seriesData,
};
}
}

View File

@@ -0,0 +1,120 @@
import { html, css, cssManager } from '@design.estate/dees-element';
import type { DeesChartBar } from './component.js';
import '@design.estate/dees-wcctools/demotools';
import './component.js';
export const demoFunc = () => {
const endpointCategories = ['/api/users', '/api/orders', '/api/products', '/api/auth', '/api/search'];
const endpointSeries = [
{ name: 'GET', data: [1240, 890, 720, 2100, 560] },
{ name: 'POST', data: [320, 450, 180, 890, 40] },
{ name: 'PUT', data: [90, 210, 150, 30, 10] },
];
const regionCategories = ['US-East', 'US-West', 'EU', 'Asia', 'Other'];
const regionSeries = [
{ name: 'Requests', data: [4500, 3200, 2800, 1900, 600] },
];
return html`
<dees-demowrapper .runAfterRender=${async (elementArg: HTMLElement) => {
const vertChart = elementArg.querySelector('#vert-chart') as DeesChartBar;
const horizChart = elementArg.querySelector('#horiz-chart') as DeesChartBar;
const stackChart = elementArg.querySelector('#stack-chart') as DeesChartBar;
const buttons = elementArg.querySelectorAll('dees-button');
buttons.forEach((button: any) => {
const text = button.text?.trim();
if (text === 'Randomize') {
button.addEventListener('click', () => {
vertChart.series = endpointSeries.map((s) => ({
...s,
data: s.data.map((v) => Math.round(v * (0.5 + Math.random()))),
}));
horizChart.series = regionSeries.map((s) => ({
...s,
data: s.data.map((v) => Math.round(v * (0.5 + Math.random()))),
}));
stackChart.series = endpointSeries.map((s) => ({
...s,
data: s.data.map((v) => Math.round(v * (0.5 + Math.random()))),
}));
});
}
});
}}>
<style>
${css`
.demoBox {
position: relative;
background: ${cssManager.bdTheme('hsl(0 0% 95%)', 'hsl(0 0% 9%)')};
height: 100%;
width: 100%;
padding: 40px;
box-sizing: border-box;
display: flex;
flex-direction: column;
gap: 24px;
}
.chartRow {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 24px;
}
.controls {
display: flex;
gap: 12px;
margin-bottom: 8px;
}
.info {
color: ${cssManager.bdTheme('hsl(215.4 16.3% 56.9%)', 'hsl(215 20.2% 55.1%)')};
font-size: 12px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Geist Sans', sans-serif;
text-align: center;
margin-top: 8px;
}
`}
</style>
<div class="demoBox">
<div class="controls">
<dees-button-group label="Actions:">
<dees-button>Randomize</dees-button>
</dees-button-group>
</div>
<div class="chartRow">
<dees-chart-bar
id="vert-chart"
.label=${'Requests by Endpoint'}
.categories=${endpointCategories}
.series=${endpointSeries}
.valueFormatter=${(val: number) => `${val} req`}
></dees-chart-bar>
<dees-chart-bar
id="horiz-chart"
.label=${'Traffic by Region'}
.categories=${regionCategories}
.series=${regionSeries}
.horizontal=${true}
.valueFormatter=${(val: number) => `${(val / 1000).toFixed(1)}k`}
></dees-chart-bar>
</div>
<dees-chart-bar
id="stack-chart"
.label=${'Stacked: Requests by Endpoint'}
.categories=${endpointCategories}
.series=${endpointSeries}
.stacked=${true}
.valueFormatter=${(val: number) => `${val} req`}
></dees-chart-bar>
<div class="info">
Bar chart with vertical, horizontal, and stacked modes •
Click 'Randomize' to update data with animation
</div>
</div>
</dees-demowrapper>
`;
};

View File

@@ -0,0 +1 @@
export * from './component.js';

View File

@@ -0,0 +1,7 @@
import { css } from '@design.estate/dees-element';
import { echartsBaseStyles } from '../dees-chart-echarts-styles.js';
export const barStyles = [
...echartsBaseStyles,
css``,
];

View File

@@ -0,0 +1,13 @@
import { html, type TemplateResult } from '@design.estate/dees-element';
import type { DeesChartBar } from './component.js';
export const renderChartBar = (component: DeesChartBar): TemplateResult => {
return html`
<dees-tile>
<div slot="header" class="chartHeader">
<span class="chartLabel">${component.label}</span>
</div>
<div class="chartContainer"></div>
</dees-tile>
`;
};

View File

@@ -0,0 +1,142 @@
import {
customElement,
property,
type TemplateResult,
} from '@design.estate/dees-element';
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, hexToRgba } from '../dees-chart-echarts-theme.js';
export interface IDonutDataItem {
name: string;
value: number;
color?: string;
}
declare global {
interface HTMLElementTagNameMap {
'dees-chart-donut': DeesChartDonut;
}
}
@customElement('dees-chart-donut')
export class DeesChartDonut extends DeesChartEchartsBase {
public static demo = demoFunc;
public static demoGroups = ['Chart'];
@property({ type: Array })
accessor data: IDonutDataItem[] = [];
@property({ type: Boolean })
accessor showLegend: boolean = true;
@property({ type: Boolean })
accessor showLabels: boolean = true;
@property({ type: String })
accessor innerRadiusPercent: string = '55%';
@property({ attribute: false })
accessor valueFormatter: (value: number) => string = (val) => `${val}`;
public static styles = donutStyles;
public render(): TemplateResult {
return renderChartDonut(this);
}
public async updated(changedProperties: Map<string, any>) {
super.updated(changedProperties);
if (
this.chartInstance &&
(changedProperties.has('data') ||
changedProperties.has('showLegend') ||
changedProperties.has('showLabels') ||
changedProperties.has('innerRadiusPercent'))
) {
this.updateChart();
}
}
protected buildOption(): Record<string, any> {
const themeColors = getThemeColors(this.goBright);
const seriesColors = getEchartsSeriesColors(this.goBright);
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;
return {
tooltip: {
trigger: 'item',
formatter: (params: any) => {
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
? {
orient: 'vertical',
right: 16,
top: 'center',
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;
},
}
: { show: false },
series: [
{
type: 'pie',
radius: [this.innerRadiusPercent, '85%'],
center: this.showLegend ? ['35%', '50%'] : ['50%', '50%'],
avoidLabelOverlap: true,
padAngle: 2,
itemStyle: {
borderRadius: 4,
},
label: this.showLabels
? {
show: true,
formatter: '{b}: {d}%',
fontSize: 11,
color: themeColors.textSecondary,
textBorderColor: 'transparent',
}
: { show: false },
data,
},
],
};
}
}

View File

@@ -0,0 +1,127 @@
import { html, css, cssManager } from '@design.estate/dees-element';
import type { DeesChartDonut } from './component.js';
import '@design.estate/dees-wcctools/demotools';
import './component.js';
export const demoFunc = () => {
const diskData = [
{ name: 'Documents', value: 42 },
{ name: 'Media', value: 28 },
{ name: 'Applications', value: 15 },
{ name: 'System', value: 10 },
{ name: 'Other', value: 5 },
];
const statusData = [
{ name: 'Healthy', value: 156 },
{ name: 'Warning', value: 23 },
{ name: 'Critical', value: 8 },
{ name: 'Unknown', value: 3 },
];
const trafficData = [
{ name: 'API', value: 45200 },
{ name: 'Static Assets', value: 23100 },
{ name: 'WebSocket', value: 12800 },
{ name: 'GraphQL', value: 8900 },
];
return html`
<dees-demowrapper .runAfterRender=${async (elementArg: HTMLElement) => {
const diskChart = elementArg.querySelector('#disk-chart') as DeesChartDonut;
const statusChart = elementArg.querySelector('#status-chart') as DeesChartDonut;
const trafficChart = elementArg.querySelector('#traffic-chart') as DeesChartDonut;
// Wire up buttons
const buttons = elementArg.querySelectorAll('dees-button');
buttons.forEach((button: any) => {
const text = button.text?.trim();
if (text === 'Randomize') {
button.addEventListener('click', () => {
diskChart.data = diskData.map((d) => ({
...d,
value: Math.round(d.value * (0.5 + Math.random())),
}));
statusChart.data = statusData.map((d) => ({
...d,
value: Math.round(d.value * (0.3 + Math.random() * 1.4)),
}));
trafficChart.data = trafficData.map((d) => ({
...d,
value: Math.round(d.value * (0.5 + Math.random())),
}));
});
}
});
}}>
<style>
${css`
.demoBox {
position: relative;
background: ${cssManager.bdTheme('hsl(0 0% 95%)', 'hsl(0 0% 9%)')};
height: 100%;
width: 100%;
padding: 40px;
box-sizing: border-box;
display: flex;
flex-direction: column;
gap: 24px;
}
.chartRow {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 24px;
}
.controls {
display: flex;
gap: 12px;
margin-bottom: 8px;
}
.info {
color: ${cssManager.bdTheme('hsl(215.4 16.3% 56.9%)', 'hsl(215 20.2% 55.1%)')};
font-size: 12px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Geist Sans', sans-serif;
text-align: center;
margin-top: 8px;
}
`}
</style>
<div class="demoBox">
<div class="controls">
<dees-button-group label="Actions:">
<dees-button>Randomize</dees-button>
</dees-button-group>
</div>
<div class="chartRow">
<dees-chart-donut
id="disk-chart"
.label=${'Disk Usage (GB)'}
.data=${diskData}
.valueFormatter=${(val: number) => `${val} GB`}
></dees-chart-donut>
<dees-chart-donut
id="status-chart"
.label=${'Service Status'}
.data=${statusData}
.valueFormatter=${(val: number) => `${val} services`}
.innerRadiusPercent=${'0%'}
></dees-chart-donut>
</div>
<dees-chart-donut
id="traffic-chart"
.label=${'Traffic Distribution'}
.data=${trafficData}
.valueFormatter=${(val: number) => `${(val / 1000).toFixed(1)}k req`}
></dees-chart-donut>
<div class="info">
Donut chart with configurable inner radius (set to 0% for full pie) •
Click 'Randomize' to update data with animation
</div>
</div>
</dees-demowrapper>
`;
};

View File

@@ -0,0 +1 @@
export * from './component.js';

View File

@@ -0,0 +1,14 @@
import { css, cssManager } from '@design.estate/dees-element';
import { echartsBaseStyles } from '../dees-chart-echarts-styles.js';
export const donutStyles = [
...echartsBaseStyles,
css`
:host {
height: 360px;
}
.chartContainer {
inset: 12px 0;
}
`,
];

View File

@@ -0,0 +1,13 @@
import { html, type TemplateResult } from '@design.estate/dees-element';
import type { DeesChartDonut } from './component.js';
export const renderChartDonut = (component: DeesChartDonut): TemplateResult => {
return html`
<dees-tile>
<div slot="header" class="chartHeader">
<span class="chartLabel">${component.label}</span>
</div>
<div class="chartContainer"></div>
</dees-tile>
`;
};

View File

@@ -0,0 +1,112 @@
import {
DeesElement,
property,
html,
type TemplateResult,
} from '@design.estate/dees-element';
import * as domtools from '@design.estate/dees-domtools';
import { DeesServiceLibLoader, type IEchartsBundle, type IEchartsInstance } from '../../services/index.js';
import { getEchartsThemeOptions } from './dees-chart-echarts-theme.js';
import '../00group-layout/dees-tile/dees-tile.js';
/**
* Abstract base class for ECharts-based chart components.
* Handles library loading, chart lifecycle, resize observation, and theme switching.
* Subclasses implement `buildOption()` to define their chart configuration.
*/
export abstract class DeesChartEchartsBase extends DeesElement {
@property()
accessor label: string = 'Untitled Chart';
protected chartInstance: IEchartsInstance | null = null;
protected echartsBundle: IEchartsBundle | null = null;
private resizeObserver: ResizeObserver | null = null;
constructor() {
super();
domtools.elementBasic.setup();
this.registerGarbageFunction(async () => {
if (this.resizeObserver) {
this.resizeObserver.disconnect();
this.resizeObserver = null;
}
if (this.chartInstance) {
try {
this.chartInstance.dispose();
this.chartInstance = null;
} catch (e) {
console.error('Error disposing ECharts instance:', e);
}
}
});
}
public render(): TemplateResult {
return html`
<dees-tile>
<div slot="header" class="chartHeader">
<span class="chartLabel">${this.label}</span>
</div>
<div class="chartContainer"></div>
</dees-tile>
`;
}
public async firstUpdated() {
await this.domtoolsPromise;
this.echartsBundle = await DeesServiceLibLoader.getInstance().loadEcharts();
await new Promise(resolve => requestAnimationFrame(resolve));
const chartContainer = this.shadowRoot!.querySelector('.chartContainer') as HTMLDivElement;
if (!chartContainer) return;
try {
this.chartInstance = this.echartsBundle.init(chartContainer, null, { renderer: 'svg' });
this.updateChart();
this.resizeObserver = new ResizeObserver(() => {
this.chartInstance?.resize();
});
this.resizeObserver.observe(chartContainer);
} catch (error) {
console.error('Failed to initialize ECharts:', error);
}
}
public async updated(changedProperties: Map<string, any>) {
super.updated(changedProperties);
if (changedProperties.has('goBright') && this.chartInstance) {
this.applyTheme();
}
}
protected abstract buildOption(): Record<string, any>;
protected updateChart(): void {
if (!this.chartInstance) return;
const themeOptions = getEchartsThemeOptions(this.goBright);
const chartOption = this.buildOption();
// 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);
}
protected applyTheme(): void {
this.updateChart();
}
public async forceResize(): Promise<void> {
this.chartInstance?.resize();
}
}

View File

@@ -0,0 +1,36 @@
import { css, cssManager } from '@design.estate/dees-element';
import { themeDefaultStyles } from '../00theme.js';
export const echartsBaseStyles = [
themeDefaultStyles,
cssManager.defaultStyles,
css`
:host {
display: block;
height: 400px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
color: var(--dees-color-text-primary);
font-size: 14px;
}
dees-tile {
height: 100%;
}
.chartHeader {
display: flex;
align-items: center;
height: 32px;
padding: 0 8px 0 16px;
}
.chartLabel {
flex: 1;
font-size: 14px;
font-weight: 500;
letter-spacing: -0.01em;
color: var(--dees-color-text-secondary);
}
.chartContainer {
position: absolute;
inset: 0;
}
`,
];

View File

@@ -0,0 +1,91 @@
/**
* Shared theme utilities for ECharts-based chart components.
* Uses the centralized themeDefaults tokens so chart colors stay in sync
* with the rest of the dees-catalog design system.
*
* ECharts renders on <svg> 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';
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: [
'#60a5fa', // blue-400 — softer in dark mode
'#2dd4bf', // teal-400
'#a78bfa', // violet-400
'#fbbf24', // amber-400
'#34d399', // emerald-400
'#fb7185', // rose-400
],
light: [
'#3b82f6', // blue-500
'#14b8a6', // teal-500
'#8b5cf6', // violet-500
'#f59e0b', // amber-500
'#10b981', // emerald-500
'#f43f5e', // rose-500
],
};
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<string, any> {
const colors = goBright ? light : dark;
return {
backgroundColor: 'transparent',
textStyle: {
color: colors.textSecondary,
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif',
fontSize: 12,
},
// 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,
textStyle: {
color: colors.textPrimary,
fontSize: 12,
},
confine: true,
},
legend: {
textStyle: {
color: colors.textSecondary,
fontSize: 12,
},
},
};
}
/**
* Helper to get the resolved theme colors object for use in buildOption().
*/
export function getThemeColors(goBright: boolean) {
return goBright ? light : dark;
}

View File

@@ -0,0 +1,161 @@
import {
customElement,
property,
type TemplateResult,
} from '@design.estate/dees-element';
import { DeesChartEchartsBase } from '../dees-chart-echarts-base.js';
import { demoFunc } from './demo.js';
import { gaugeStyles } from './styles.js';
import { renderChartGauge } from './template.js';
import { getEchartsSeriesColors, getThemeColors } from '../dees-chart-echarts-theme.js';
export interface IGaugeThreshold {
value: number;
color: string;
}
declare global {
interface HTMLElementTagNameMap {
'dees-chart-gauge': DeesChartGauge;
}
}
@customElement('dees-chart-gauge')
export class DeesChartGauge extends DeesChartEchartsBase {
public static demo = demoFunc;
public static demoGroups = ['Chart'];
@property({ type: Number })
accessor value: number = 0;
@property({ type: Number })
accessor min: number = 0;
@property({ type: Number })
accessor max: number = 100;
@property({ type: String })
accessor unit: string = '%';
@property({ type: Array })
accessor thresholds: IGaugeThreshold[] = [];
@property({ type: Boolean })
accessor showTicks: boolean = true;
public static styles = gaugeStyles;
public render(): TemplateResult {
return renderChartGauge(this);
}
public async updated(changedProperties: Map<string, any>) {
super.updated(changedProperties);
if (
this.chartInstance &&
(changedProperties.has('value') ||
changedProperties.has('min') ||
changedProperties.has('max') ||
changedProperties.has('unit') ||
changedProperties.has('thresholds') ||
changedProperties.has('showTicks'))
) {
this.updateChart();
}
}
protected buildOption(): Record<string, any> {
const colors = getThemeColors(this.goBright);
const seriesColors = getEchartsSeriesColors(this.goBright);
const primaryColor = seriesColors[0];
// Build axis line color stops from thresholds
let axisLineColors: Array<[number, string]>;
if (this.thresholds.length > 0) {
const sorted = [...this.thresholds].sort((a, b) => a.value - b.value);
axisLineColors = sorted.map((t) => [
(t.value - this.min) / (this.max - this.min),
t.color,
]);
// Ensure we end at 1
if (axisLineColors[axisLineColors.length - 1][0] < 1) {
axisLineColors.push([1, sorted[sorted.length - 1].color]);
}
} else {
axisLineColors = [[1, primaryColor]];
}
return {
series: [
{
type: 'gauge',
min: this.min,
max: this.max,
startAngle: 220,
endAngle: -40,
progress: {
show: true,
width: 14,
roundCap: true,
},
pointer: {
show: true,
length: '60%',
width: 5,
itemStyle: {
color: 'auto',
},
},
axisLine: {
lineStyle: {
width: 14,
color: axisLineColors,
opacity: 0.3,
},
},
axisTick: {
show: this.showTicks,
distance: -20,
length: 6,
lineStyle: {
color: colors.borderStrong,
width: 1,
},
},
splitLine: {
show: this.showTicks,
distance: -24,
length: 10,
lineStyle: {
color: colors.textMuted,
width: 2,
},
},
axisLabel: {
show: this.showTicks,
distance: 30,
color: colors.textMuted,
fontSize: 11,
},
detail: {
valueAnimation: true,
fontSize: 28,
fontWeight: 600,
offsetCenter: [0, '65%'],
color: colors.textPrimary,
formatter: `{value}${this.unit}`,
},
title: {
show: false,
},
data: [
{
value: this.value,
},
],
},
],
};
}
}

View File

@@ -0,0 +1,125 @@
import { html, css, cssManager } from '@design.estate/dees-element';
import type { DeesChartGauge } from './component.js';
import '@design.estate/dees-wcctools/demotools';
import './component.js';
export const demoFunc = () => {
const defaultThresholds = [
{ value: 60, color: 'hsl(142 76% 36%)' },
{ value: 80, color: 'hsl(38 92% 50%)' },
{ value: 100, color: 'hsl(0 72% 50%)' },
];
return html`
<dees-demowrapper .runAfterRender=${async (elementArg: HTMLElement) => {
const cpuGauge = elementArg.querySelector('#cpu-gauge') as DeesChartGauge;
const memGauge = elementArg.querySelector('#mem-gauge') as DeesChartGauge;
const slaGauge = elementArg.querySelector('#sla-gauge') as DeesChartGauge;
let animInterval: number | null = null;
const buttons = elementArg.querySelectorAll('dees-button');
buttons.forEach((button: any) => {
const text = button.text?.trim();
if (text === 'Animate') {
button.addEventListener('click', () => {
if (animInterval) return;
animInterval = window.setInterval(() => {
cpuGauge.value = Math.round(30 + Math.random() * 60);
memGauge.value = Math.round(40 + Math.random() * 50);
slaGauge.value = Math.round((95 + Math.random() * 5) * 100) / 100;
}, 2000);
});
} else if (text === 'Stop') {
button.addEventListener('click', () => {
if (animInterval) {
window.clearInterval(animInterval);
animInterval = null;
}
});
} else if (text === 'Spike') {
button.addEventListener('click', () => {
cpuGauge.value = 95;
memGauge.value = 88;
slaGauge.value = 96.5;
});
}
});
}}>
<style>
${css`
.demoBox {
position: relative;
background: ${cssManager.bdTheme('hsl(0 0% 95%)', 'hsl(0 0% 9%)')};
height: 100%;
width: 100%;
padding: 40px;
box-sizing: border-box;
display: flex;
flex-direction: column;
gap: 24px;
}
.gaugeRow {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 24px;
}
.controls {
display: flex;
gap: 12px;
margin-bottom: 8px;
}
.info {
color: ${cssManager.bdTheme('hsl(215.4 16.3% 56.9%)', 'hsl(215 20.2% 55.1%)')};
font-size: 12px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Geist Sans', sans-serif;
text-align: center;
margin-top: 8px;
}
`}
</style>
<div class="demoBox">
<div class="controls">
<dees-button-group label="Actions:">
<dees-button>Animate</dees-button>
<dees-button>Stop</dees-button>
<dees-button>Spike</dees-button>
</dees-button-group>
</div>
<div class="gaugeRow">
<dees-chart-gauge
id="cpu-gauge"
.label=${'CPU Usage'}
.value=${42}
.unit=${'%'}
.thresholds=${defaultThresholds}
></dees-chart-gauge>
<dees-chart-gauge
id="mem-gauge"
.label=${'Memory Usage'}
.value=${67}
.unit=${'%'}
.thresholds=${defaultThresholds}
></dees-chart-gauge>
<dees-chart-gauge
id="sla-gauge"
.label=${'SLA Uptime'}
.value=${99.8}
.min=${95}
.max=${100}
.unit=${'%'}
.showTicks=${true}
></dees-chart-gauge>
</div>
<div class="info">
Gauge chart with animated value transitions and threshold coloring •
Click 'Animate' for live updates, 'Spike' to simulate high load
</div>
</div>
</dees-demowrapper>
`;
};

View File

@@ -0,0 +1 @@
export * from './component.js';

View File

@@ -0,0 +1,11 @@
import { css } from '@design.estate/dees-element';
import { echartsBaseStyles } from '../dees-chart-echarts-styles.js';
export const gaugeStyles = [
...echartsBaseStyles,
css`
:host {
height: 320px;
}
`,
];

View File

@@ -0,0 +1,13 @@
import { html, type TemplateResult } from '@design.estate/dees-element';
import type { DeesChartGauge } from './component.js';
export const renderChartGauge = (component: DeesChartGauge): TemplateResult => {
return html`
<dees-tile>
<div slot="header" class="chartHeader">
<span class="chartLabel">${component.label}</span>
</div>
<div class="chartContainer"></div>
</dees-tile>
`;
};

View File

@@ -0,0 +1,132 @@
import {
customElement,
property,
type TemplateResult,
} from '@design.estate/dees-element';
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, hexToRgba } from '../dees-chart-echarts-theme.js';
export interface IRadarIndicator {
name: string;
max: number;
}
export interface IRadarSeriesItem {
name: string;
values: number[];
color?: string;
}
declare global {
interface HTMLElementTagNameMap {
'dees-chart-radar': DeesChartRadar;
}
}
@customElement('dees-chart-radar')
export class DeesChartRadar extends DeesChartEchartsBase {
public static demo = demoFunc;
public static demoGroups = ['Chart'];
@property({ type: Array })
accessor indicators: IRadarIndicator[] = [];
@property({ type: Array })
accessor series: IRadarSeriesItem[] = [];
@property({ type: Boolean })
accessor showLegend: boolean = true;
@property({ type: Boolean })
accessor fillArea: boolean = true;
public static styles = radarStyles;
public render(): TemplateResult {
return renderChartRadar(this);
}
public async updated(changedProperties: Map<string, any>) {
super.updated(changedProperties);
if (
this.chartInstance &&
(changedProperties.has('indicators') ||
changedProperties.has('series') ||
changedProperties.has('showLegend') ||
changedProperties.has('fillArea'))
) {
this.updateChart();
}
}
protected buildOption(): Record<string, any> {
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, borderColor: color, borderWidth: 1 },
lineStyle: { color, width: 1.5 },
areaStyle: this.fillArea ? { color: hexToRgba(color, fillAlpha) } : undefined,
symbol: 'circle',
symbolSize: 5,
};
});
return {
tooltip: {
trigger: 'item',
},
legend: this.showLegend && this.series.length > 1
? { bottom: 8, itemWidth: 10, itemHeight: 10 }
: { show: false },
radar: {
indicator: this.indicators.map((ind) => ({
name: ind.name,
max: ind.max,
})),
shape: 'polygon',
splitNumber: 4,
axisName: {
color: colors.textSecondary,
fontSize: 11,
},
splitArea: {
areaStyle: {
color: [colors.bgTertiary, colors.bgSecondary],
},
},
splitLine: {
lineStyle: {
color: colors.borderDefault,
},
},
axisLine: {
lineStyle: {
color: colors.borderDefault,
},
},
},
series: [
{
type: 'radar',
data: seriesData,
emphasis: {
lineStyle: {
width: 3,
},
},
},
],
};
}
}

View File

@@ -0,0 +1,119 @@
import { html, css, cssManager } from '@design.estate/dees-element';
import type { DeesChartRadar } from './component.js';
import '@design.estate/dees-wcctools/demotools';
import './component.js';
export const demoFunc = () => {
const indicators = [
{ name: 'Latency', max: 100 },
{ name: 'Throughput', max: 100 },
{ name: 'Availability', max: 100 },
{ name: 'Error Rate', max: 100 },
{ name: 'Saturation', max: 100 },
{ name: 'Security', max: 100 },
];
const series = [
{ name: 'Service A', values: [85, 90, 99, 12, 45, 78] },
{ name: 'Service B', values: [70, 65, 95, 28, 60, 90] },
];
const singleIndicators = [
{ name: 'Speed', max: 10 },
{ name: 'Reliability', max: 10 },
{ name: 'Comfort', max: 10 },
{ name: 'Safety', max: 10 },
{ name: 'Cost', max: 10 },
];
const singleSeries = [
{ name: 'Rating', values: [8.5, 9.2, 7.0, 9.5, 6.0] },
];
return html`
<dees-demowrapper .runAfterRender=${async (elementArg: HTMLElement) => {
const compChart = elementArg.querySelector('#comparison-chart') as DeesChartRadar;
const singleChart = elementArg.querySelector('#single-chart') as DeesChartRadar;
const buttons = elementArg.querySelectorAll('dees-button');
buttons.forEach((button: any) => {
const text = button.text?.trim();
if (text === 'Randomize') {
button.addEventListener('click', () => {
compChart.series = series.map((s) => ({
...s,
values: s.values.map(() => Math.round(20 + Math.random() * 80)),
}));
singleChart.series = singleSeries.map((s) => ({
...s,
values: s.values.map(() => Math.round((2 + Math.random() * 8) * 10) / 10),
}));
});
}
});
}}>
<style>
${css`
.demoBox {
position: relative;
background: ${cssManager.bdTheme('hsl(0 0% 95%)', 'hsl(0 0% 9%)')};
height: 100%;
width: 100%;
padding: 40px;
box-sizing: border-box;
display: flex;
flex-direction: column;
gap: 24px;
}
.chartRow {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 24px;
}
.controls {
display: flex;
gap: 12px;
margin-bottom: 8px;
}
.info {
color: ${cssManager.bdTheme('hsl(215.4 16.3% 56.9%)', 'hsl(215 20.2% 55.1%)')};
font-size: 12px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Geist Sans', sans-serif;
text-align: center;
margin-top: 8px;
}
`}
</style>
<div class="demoBox">
<div class="controls">
<dees-button-group label="Actions:">
<dees-button>Randomize</dees-button>
</dees-button-group>
</div>
<div class="chartRow">
<dees-chart-radar
id="comparison-chart"
.label=${'Service Health Comparison'}
.indicators=${indicators}
.series=${series}
></dees-chart-radar>
<dees-chart-radar
id="single-chart"
.label=${'Product Rating'}
.indicators=${singleIndicators}
.series=${singleSeries}
.fillArea=${true}
></dees-chart-radar>
</div>
<div class="info">
Radar chart for multi-dimensional comparison •
Supports multiple overlay series and configurable fill •
Click 'Randomize' to update data
</div>
</div>
</dees-demowrapper>
`;
};

View File

@@ -0,0 +1 @@
export * from './component.js';

View File

@@ -0,0 +1,7 @@
import { css } from '@design.estate/dees-element';
import { echartsBaseStyles } from '../dees-chart-echarts-styles.js';
export const radarStyles = [
...echartsBaseStyles,
css``,
];

View File

@@ -0,0 +1,13 @@
import { html, type TemplateResult } from '@design.estate/dees-element';
import type { DeesChartRadar } from './component.js';
export const renderChartRadar = (component: DeesChartRadar): TemplateResult => {
return html`
<dees-tile>
<div slot="header" class="chartHeader">
<span class="chartLabel">${component.label}</span>
</div>
<div class="chartContainer"></div>
</dees-tile>
`;
};

View File

@@ -1,3 +1,7 @@
// Chart Components
export * from './dees-chart-area/index.js';
export * from './dees-chart-bar/index.js';
export * from './dees-chart-donut/index.js';
export * from './dees-chart-gauge/index.js';
export * from './dees-chart-log/index.js';
export * from './dees-chart-radar/index.js';

View File

@@ -281,7 +281,7 @@ export class DeesStatsGrid extends DeesElement {
.gauge-background {
fill: none;
stroke: ${cssManager.bdTheme('#e8e8e8', '#1a1a1a')};
stroke: var(--dees-color-border-default);
stroke-width: 6;
}
@@ -329,7 +329,7 @@ export class DeesStatsGrid extends DeesElement {
.percentage-bar {
width: 100%;
height: 6px;
background: ${cssManager.bdTheme('#e8e8e8', '#1a1a1a')};
background: var(--dees-color-border-default);
border-radius: 3px;
overflow: hidden;
margin-top: auto;
@@ -337,7 +337,7 @@ export class DeesStatsGrid extends DeesElement {
.percentage-fill {
height: 100%;
background: ${cssManager.bdTheme('#333333', '#e0e0e0')};
background: var(--dees-color-text-muted);
transition: width 0.6s cubic-bezier(0.4, 0, 0.2, 1);
border-radius: 3px;
}
@@ -386,14 +386,14 @@ export class DeesStatsGrid extends DeesElement {
.multi-percentage-bar {
width: 100%;
height: 4px;
background: ${cssManager.bdTheme('#e8e8e8', '#1a1a1a')};
background: var(--dees-color-border-default);
border-radius: 2px;
overflow: hidden;
}
.multi-percentage-fill {
height: 100%;
background: ${cssManager.bdTheme('#333333', '#e0e0e0')};
background: var(--dees-color-text-muted);
transition: width 0.6s cubic-bezier(0.4, 0, 0.2, 1);
border-radius: 2px;
}
@@ -464,7 +464,7 @@ export class DeesStatsGrid extends DeesElement {
.cpu-core-bar-wrapper {
flex: 1;
width: 100%;
background: ${cssManager.bdTheme('#e8e8e8', '#1a1a1a')};
background: var(--dees-color-border-default);
border-radius: 2px;
position: relative;
overflow: hidden;
@@ -477,21 +477,21 @@ export class DeesStatsGrid extends DeesElement {
left: 0;
right: 0;
width: 100%;
background: ${cssManager.bdTheme('#666666', '#888888')};
background: var(--dees-color-text-muted);
transition: height 0.3s cubic-bezier(0.4, 0, 0.2, 1), background-color 0.3s ease;
border-radius: 2px 2px 0 0;
}
.cpu-core-bar-fill.low {
background: ${cssManager.bdTheme('hsl(142.1 76.2% 36.3%)', 'hsl(142.1 70.6% 45.3%)')};
background: var(--dees-color-accent-success);
}
.cpu-core-bar-fill.medium {
background: ${cssManager.bdTheme('hsl(45.4 93.4% 47.5%)', 'hsl(45.4 93.4% 47.5%)')};
background: var(--dees-color-accent-warning);
}
.cpu-core-bar-fill.high {
background: ${cssManager.bdTheme('hsl(0 84.2% 60.2%)', 'hsl(0 84.2% 60.2%)')};
background: var(--dees-color-accent-error);
}
.cpu-core-label {
@@ -531,24 +531,24 @@ export class DeesStatsGrid extends DeesElement {
.partition-bar {
width: 100%;
height: 6px;
background: ${cssManager.bdTheme('#e8e8e8', '#1a1a1a')};
background: var(--dees-color-border-default);
border-radius: 3px;
overflow: hidden;
}
.partition-bar-fill {
height: 100%;
background: ${cssManager.bdTheme('#333333', '#e0e0e0')};
background: var(--dees-color-text-muted);
transition: width 0.6s cubic-bezier(0.4, 0, 0.2, 1);
border-radius: 3px;
}
.partition-bar-fill.warning {
background: ${cssManager.bdTheme('hsl(45.4 93.4% 47.5%)', 'hsl(45.4 93.4% 47.5%)')};
background: var(--dees-color-accent-warning);
}
.partition-bar-fill.critical {
background: ${cssManager.bdTheme('hsl(0 84.2% 60.2%)', 'hsl(0 84.2% 60.2%)')};
background: var(--dees-color-accent-error);
}
.partition-stats {
@@ -695,7 +695,7 @@ export class DeesStatsGrid extends DeesElement {
.disk-health-bar {
width: 100%;
height: 4px;
background: ${cssManager.bdTheme('#e8e8e8', '#1a1a1a')};
background: var(--dees-color-border-default);
border-radius: 2px;
overflow: hidden;
}
@@ -771,7 +771,7 @@ export class DeesStatsGrid extends DeesElement {
.trend-line {
fill: none;
stroke: ${cssManager.bdTheme('#999999', '#666666')};
stroke: var(--dees-color-text-muted);
stroke-width: 1.5;
stroke-linejoin: round;
stroke-linecap: round;

View File

@@ -74,6 +74,28 @@ export interface ILightweightChartsBundle {
};
}
/**
* Minimal type for an ECharts instance (loaded from CDN)
*/
export interface IEchartsInstance {
setOption(option: Record<string, any>, notMerge?: boolean): void;
resize(opts?: { width?: number; height?: number }): void;
dispose(): void;
on(eventName: string, handler: (...args: any[]) => void): void;
off(eventName: string, handler?: (...args: any[]) => void): void;
getOption(): Record<string, any>;
clear(): void;
}
/**
* Bundle type for Apache ECharts
*/
export interface IEchartsBundle {
init: (dom: HTMLElement, theme?: string | object | null, opts?: Record<string, any>) => IEchartsInstance;
dispose: (chart: IEchartsInstance | HTMLElement | string) => void;
getInstanceByDom: (dom: HTMLElement) => IEchartsInstance | undefined;
}
/**
* Bundle type for Tiptap editor and extensions
*/
@@ -108,6 +130,7 @@ export class DeesServiceLibLoader {
private xtermSearchAddonLib: IXtermSearchAddonBundle | null = null;
private highlightJsLib: HLJSApi | null = null;
private lightweightChartsLib: ILightweightChartsBundle | null = null;
private echartsLib: IEchartsBundle | null = null;
private tiptapLib: ITiptapBundle | null = null;
// Loading promises to prevent duplicate concurrent loads
@@ -116,6 +139,7 @@ export class DeesServiceLibLoader {
private xtermSearchAddonLoadingPromise: Promise<IXtermSearchAddonBundle> | null = null;
private highlightJsLoadingPromise: Promise<HLJSApi> | null = null;
private lightweightChartsLoadingPromise: Promise<ILightweightChartsBundle> | null = null;
private echartsLoadingPromise: Promise<IEchartsBundle> | null = null;
private tiptapLoadingPromise: Promise<ITiptapBundle> | null = null;
private constructor() {}
@@ -296,6 +320,36 @@ body > div[style*="top: -50000px"][style*="width: 50000px"] {
return this.lightweightChartsLoadingPromise;
}
/**
* Load Apache ECharts from CDN
* @returns Promise resolving to ECharts bundle
*/
public async loadEcharts(): Promise<IEchartsBundle> {
if (this.echartsLib) {
return this.echartsLib;
}
if (this.echartsLoadingPromise) {
return this.echartsLoadingPromise;
}
this.echartsLoadingPromise = (async () => {
// Use the pre-built ESM bundle which includes all chart types and components.
// The +esm wrapper only exports the core without auto-registered chart types.
const url = `${CDN_BASE}/echarts@${CDN_VERSIONS.echarts}/dist/echarts.esm.min.js`;
const module = await import(/* @vite-ignore */ url);
this.echartsLib = {
init: module.init,
dispose: module.dispose,
getInstanceByDom: module.getInstanceByDom,
};
return this.echartsLib;
})();
return this.echartsLoadingPromise;
}
/**
* Load Tiptap rich text editor and extensions from CDN
* @returns Promise resolving to Tiptap bundle with Editor and extensions
@@ -348,6 +402,7 @@ body > div[style*="top: -50000px"][style*="width: 50000px"] {
this.loadXtermSearchAddon(),
this.loadHighlightJs(),
this.loadLightweightCharts(),
this.loadEcharts(),
this.loadTiptap(),
]);
}
@@ -355,7 +410,7 @@ body > div[style*="top: -50000px"][style*="width: 50000px"] {
/**
* Check if a specific library is already loaded
*/
public isLoaded(library: 'xterm' | 'xtermFitAddon' | 'xtermSearchAddon' | 'highlightJs' | 'lightweightCharts' | 'tiptap'): boolean {
public isLoaded(library: 'xterm' | 'xtermFitAddon' | 'xtermSearchAddon' | 'highlightJs' | 'lightweightCharts' | 'echarts' | 'tiptap'): boolean {
switch (library) {
case 'xterm':
return this.xtermLib !== null;
@@ -367,6 +422,8 @@ body > div[style*="top: -50000px"][style*="width: 50000px"] {
return this.highlightJsLib !== null;
case 'lightweightCharts':
return this.lightweightChartsLib !== null;
case 'echarts':
return this.echartsLib !== null;
case 'tiptap':
return this.tiptapLib !== null;
default:

View File

@@ -1,3 +1,3 @@
export { DeesServiceLibLoader } from './DeesServiceLibLoader.js';
export type { IXtermBundle, IXtermFitAddonBundle, IXtermSearchAddonBundle, IXtermSearchAddon, ITiptapBundle, ILightweightChartsBundle } from './DeesServiceLibLoader.js';
export type { IXtermBundle, IXtermFitAddonBundle, IXtermSearchAddonBundle, IXtermSearchAddon, ITiptapBundle, ILightweightChartsBundle, IEchartsBundle, IEchartsInstance } from './DeesServiceLibLoader.js';
export { CDN_BASE, CDN_VERSIONS } from './versions.js';

View File

@@ -7,6 +7,7 @@ export const CDN_VERSIONS = {
xtermAddonFit: '0.8.0',
xtermAddonSearch: '0.13.0',
highlightJs: '11.11.1',
echarts: '5.6.0',
lightweightCharts: '5.1.0',
tiptap: '2.27.2',
fontawesome: '7.2.0',