@blukavantui/charts-community 0.0.0-stage → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.txt +103 -0
- package/README.md +98 -2
- package/THIRD_PARTY_NOTICES.txt +288 -0
- package/dist/accessibility.d.ts +53 -0
- package/dist/async.d.ts +2 -0
- package/dist/bar.css +1 -0
- package/dist/bar.d.ts +5 -0
- package/dist/bar.js +1 -0
- package/dist/browser/charts-community.css +1 -0
- package/dist/browser/charts-community.js +52 -0
- package/dist/build-audit.json +209 -0
- package/dist/builders/bar.d.ts +6 -0
- package/dist/builders/doughnut.d.ts +6 -0
- package/dist/builders/heatmap.d.ts +16 -0
- package/dist/builders/line.d.ts +6 -0
- package/dist/builders/pie.d.ts +6 -0
- package/dist/builders/radar.d.ts +6 -0
- package/dist/builders/scatter.d.ts +6 -0
- package/dist/catalogue-model.d.ts +11 -0
- package/dist/catalogue-runtime.d.ts +23 -0
- package/dist/catalogue-types.d.ts +167 -0
- package/dist/chart-state.d.ts +18 -0
- package/dist/chunk-2GUWWKHW.js +1 -0
- package/dist/chunk-3PITHTEO.js +1 -0
- package/dist/chunk-4B5MQ6GZ.js +1 -0
- package/dist/chunk-4EQMOBBN.js +1 -0
- package/dist/chunk-4WDZUJDS.js +1 -0
- package/dist/chunk-5NUOOMHV.js +1 -0
- package/dist/chunk-7OCMRHIH.js +1 -0
- package/dist/chunk-CF5Q2DDP.js +1 -0
- package/dist/chunk-EITBQBN2.js +1 -0
- package/dist/chunk-FXNKAHRG.js +1 -0
- package/dist/chunk-IDMAFTWV.js +1 -0
- package/dist/chunk-ILTENI35.js +1 -0
- package/dist/chunk-KRMM7EWI.js +1 -0
- package/dist/chunk-LUUPBBD4.js +1 -0
- package/dist/chunk-MR5VITXX.js +1 -0
- package/dist/chunk-NASBRHIW.js +1 -0
- package/dist/chunk-NXDBBMF5.js +1 -0
- package/dist/chunk-OQ6MP7PY.js +1 -0
- package/dist/chunk-RC2TCMJF.js +1 -0
- package/dist/chunk-RXOGAN23.js +1 -0
- package/dist/chunk-UGXWOWAG.js +2 -0
- package/dist/chunk-UO2KNHTU.js +2 -0
- package/dist/chunk-UQMMBG26.js +3 -0
- package/dist/chunk-VTJ4XPRV.js +1 -0
- package/dist/chunk-WJ3UHH6J.js +1 -0
- package/dist/chunk-XLJUSNGV.js +1 -0
- package/dist/colors.d.ts +15 -0
- package/dist/community.d.ts +11 -0
- package/dist/doughnut.css +1 -0
- package/dist/doughnut.d.ts +5 -0
- package/dist/doughnut.js +1 -0
- package/dist/export.d.ts +5 -0
- package/dist/fonts/dm-sans-LICENSE.txt +93 -0
- package/dist/fonts/dm-sans-latin-ext-wght-italic.woff2 +0 -0
- package/dist/fonts/dm-sans-latin-ext-wght-normal.woff2 +0 -0
- package/dist/fonts/dm-sans-latin-wght-italic.woff2 +0 -0
- package/dist/fonts/dm-sans-latin-wght-normal.woff2 +0 -0
- package/dist/fonts/ibm-plex-mono-LICENSE.txt +93 -0
- package/dist/fonts/ibm-plex-mono-latin-100-italic.woff2 +0 -0
- package/dist/fonts/ibm-plex-mono-latin-100-normal.woff2 +0 -0
- package/dist/fonts/ibm-plex-mono-latin-200-italic.woff2 +0 -0
- package/dist/fonts/ibm-plex-mono-latin-200-normal.woff2 +0 -0
- package/dist/fonts/ibm-plex-mono-latin-300-italic.woff2 +0 -0
- package/dist/fonts/ibm-plex-mono-latin-300-normal.woff2 +0 -0
- package/dist/fonts/ibm-plex-mono-latin-400-italic.woff2 +0 -0
- package/dist/fonts/ibm-plex-mono-latin-400-normal.woff2 +0 -0
- package/dist/fonts/ibm-plex-mono-latin-500-italic.woff2 +0 -0
- package/dist/fonts/ibm-plex-mono-latin-500-normal.woff2 +0 -0
- package/dist/fonts/ibm-plex-mono-latin-600-italic.woff2 +0 -0
- package/dist/fonts/ibm-plex-mono-latin-600-normal.woff2 +0 -0
- package/dist/fonts/ibm-plex-mono-latin-700-italic.woff2 +0 -0
- package/dist/fonts/ibm-plex-mono-latin-700-normal.woff2 +0 -0
- package/dist/fonts/ibm-plex-mono-latin-ext-100-italic.woff2 +0 -0
- package/dist/fonts/ibm-plex-mono-latin-ext-100-normal.woff2 +0 -0
- package/dist/fonts/ibm-plex-mono-latin-ext-200-italic.woff2 +0 -0
- package/dist/fonts/ibm-plex-mono-latin-ext-200-normal.woff2 +0 -0
- package/dist/fonts/ibm-plex-mono-latin-ext-300-italic.woff2 +0 -0
- package/dist/fonts/ibm-plex-mono-latin-ext-300-normal.woff2 +0 -0
- package/dist/fonts/ibm-plex-mono-latin-ext-400-italic.woff2 +0 -0
- package/dist/fonts/ibm-plex-mono-latin-ext-400-normal.woff2 +0 -0
- package/dist/fonts/ibm-plex-mono-latin-ext-500-italic.woff2 +0 -0
- package/dist/fonts/ibm-plex-mono-latin-ext-500-normal.woff2 +0 -0
- package/dist/fonts/ibm-plex-mono-latin-ext-600-italic.woff2 +0 -0
- package/dist/fonts/ibm-plex-mono-latin-ext-600-normal.woff2 +0 -0
- package/dist/fonts/ibm-plex-mono-latin-ext-700-italic.woff2 +0 -0
- package/dist/fonts/ibm-plex-mono-latin-ext-700-normal.woff2 +0 -0
- package/dist/fonts/inter-LICENSE.txt +93 -0
- package/dist/fonts/inter-latin-ext-wght-italic.woff2 +0 -0
- package/dist/fonts/inter-latin-ext-wght-normal.woff2 +0 -0
- package/dist/fonts/inter-latin-wght-italic.woff2 +0 -0
- package/dist/fonts/inter-latin-wght-normal.woff2 +0 -0
- package/dist/fonts/jetbrains-mono-LICENSE.txt +93 -0
- package/dist/fonts/jetbrains-mono-latin-ext-wght-italic.woff2 +0 -0
- package/dist/fonts/jetbrains-mono-latin-ext-wght-normal.woff2 +0 -0
- package/dist/fonts/jetbrains-mono-latin-wght-italic.woff2 +0 -0
- package/dist/fonts/jetbrains-mono-latin-wght-normal.woff2 +0 -0
- package/dist/fonts/lora-LICENSE.txt +93 -0
- package/dist/fonts/lora-latin-ext-wght-italic.woff2 +0 -0
- package/dist/fonts/lora-latin-ext-wght-normal.woff2 +0 -0
- package/dist/fonts/lora-latin-wght-italic.woff2 +0 -0
- package/dist/fonts/lora-latin-wght-normal.woff2 +0 -0
- package/dist/fonts/manifest.json +618 -0
- package/dist/fonts/manrope-LICENSE.txt +93 -0
- package/dist/fonts/manrope-latin-ext-wght-normal.woff2 +0 -0
- package/dist/fonts/manrope-latin-wght-normal.woff2 +0 -0
- package/dist/fonts/merriweather-LICENSE.txt +93 -0
- package/dist/fonts/merriweather-latin-ext-wght-italic.woff2 +0 -0
- package/dist/fonts/merriweather-latin-ext-wght-normal.woff2 +0 -0
- package/dist/fonts/merriweather-latin-wght-italic.woff2 +0 -0
- package/dist/fonts/merriweather-latin-wght-normal.woff2 +0 -0
- package/dist/fonts/montserrat-LICENSE.txt +93 -0
- package/dist/fonts/montserrat-latin-ext-wght-italic.woff2 +0 -0
- package/dist/fonts/montserrat-latin-ext-wght-normal.woff2 +0 -0
- package/dist/fonts/montserrat-latin-wght-italic.woff2 +0 -0
- package/dist/fonts/montserrat-latin-wght-normal.woff2 +0 -0
- package/dist/fonts/playfair-display-LICENSE.txt +93 -0
- package/dist/fonts/playfair-display-latin-ext-wght-italic.woff2 +0 -0
- package/dist/fonts/playfair-display-latin-ext-wght-normal.woff2 +0 -0
- package/dist/fonts/playfair-display-latin-wght-italic.woff2 +0 -0
- package/dist/fonts/playfair-display-latin-wght-normal.woff2 +0 -0
- package/dist/fonts/roboto-LICENSE.txt +93 -0
- package/dist/fonts/roboto-latin-ext-wght-italic.woff2 +0 -0
- package/dist/fonts/roboto-latin-ext-wght-normal.woff2 +0 -0
- package/dist/fonts/roboto-latin-wght-italic.woff2 +0 -0
- package/dist/fonts/roboto-latin-wght-normal.woff2 +0 -0
- package/dist/fonts/source-sans-3-LICENSE.txt +93 -0
- package/dist/fonts/source-sans-3-latin-ext-wght-italic.woff2 +0 -0
- package/dist/fonts/source-sans-3-latin-ext-wght-normal.woff2 +0 -0
- package/dist/fonts/source-sans-3-latin-wght-italic.woff2 +0 -0
- package/dist/fonts/source-sans-3-latin-wght-normal.woff2 +0 -0
- package/dist/heatmap.css +1 -0
- package/dist/heatmap.d.ts +3 -0
- package/dist/heatmap.js +1 -0
- package/dist/index.css +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +1 -0
- package/dist/legend-export.d.ts +9 -0
- package/dist/legend-model.d.ts +39 -0
- package/dist/legend.d.ts +19 -0
- package/dist/limits.d.ts +16 -0
- package/dist/line.css +1 -0
- package/dist/line.d.ts +5 -0
- package/dist/line.js +1 -0
- package/dist/loading-state.d.ts +6 -0
- package/dist/map-assets.d.ts +4 -0
- package/dist/map-resources.d.ts +24 -0
- package/dist/messages.d.ts +3 -0
- package/dist/model.d.ts +41 -0
- package/dist/motion/bar.css +1 -0
- package/dist/motion/bar.d.ts +8 -0
- package/dist/motion/bar.js +1 -0
- package/dist/motion/doughnut.css +1 -0
- package/dist/motion/doughnut.d.ts +8 -0
- package/dist/motion/doughnut.js +1 -0
- package/dist/motion/heatmap.css +1 -0
- package/dist/motion/heatmap.d.ts +7 -0
- package/dist/motion/heatmap.js +1 -0
- package/dist/motion/line.css +1 -0
- package/dist/motion/line.d.ts +8 -0
- package/dist/motion/line.js +1 -0
- package/dist/motion/pie.css +1 -0
- package/dist/motion/pie.d.ts +8 -0
- package/dist/motion/pie.js +1 -0
- package/dist/motion/radar.css +1 -0
- package/dist/motion/radar.d.ts +8 -0
- package/dist/motion/radar.js +1 -0
- package/dist/motion/scatter.css +1 -0
- package/dist/motion/scatter.d.ts +8 -0
- package/dist/motion/scatter.js +1 -0
- package/dist/motion/sparkline.d.ts +7 -0
- package/dist/motion/sparkline.js +1 -0
- package/dist/motion-collection.d.ts +7 -0
- package/dist/motion-community.d.ts +40 -0
- package/dist/motion-policy.d.ts +14 -0
- package/dist/motion.css +1 -0
- package/dist/motion.d.ts +40 -0
- package/dist/motion.js +1 -0
- package/dist/options.d.ts +11 -0
- package/dist/pie.css +1 -0
- package/dist/pie.d.ts +5 -0
- package/dist/pie.js +1 -0
- package/dist/radar.css +1 -0
- package/dist/radar.d.ts +4 -0
- package/dist/radar.js +1 -0
- package/dist/runtime.d.ts +4 -0
- package/dist/scatter.css +1 -0
- package/dist/scatter.d.ts +5 -0
- package/dist/scatter.js +1 -0
- package/dist/sparkline-runtime/runtime.d.ts +10 -0
- package/dist/sparkline-validation.d.ts +3 -0
- package/dist/sparkline.css +25 -0
- package/dist/sparkline.d.ts +44 -0
- package/dist/sparkline.js +1 -0
- package/dist/state-announcement.d.ts +6 -0
- package/dist/styles.css +496 -0
- package/dist/theme.css +1391 -0
- package/dist/types.d.ts +137 -0
- package/dist/validation.d.ts +8 -0
- package/dist/web-api.json +4305 -0
- package/dist/web-motion-sparkline.d.ts +4 -0
- package/edition.json +48 -0
- package/package.json +103 -4
- package/src/accessibility.ts +81 -0
- package/src/assets.d.ts +1 -0
- package/src/async.ts +12 -0
- package/src/bar.ts +13 -0
- package/src/builders/bar.ts +39 -0
- package/src/builders/doughnut.ts +66 -0
- package/src/builders/heatmap.ts +60 -0
- package/src/builders/line.ts +39 -0
- package/src/builders/pie.ts +66 -0
- package/src/builders/radar.ts +65 -0
- package/src/builders/scatter.ts +34 -0
- package/src/catalogue-model.ts +235 -0
- package/src/catalogue-runtime.ts +244 -0
- package/src/catalogue-types.ts +163 -0
- package/src/chart-state.ts +35 -0
- package/src/charts.css +135 -0
- package/src/colors.ts +277 -0
- package/src/community.ts +11 -0
- package/src/doughnut.ts +12 -0
- package/src/export.ts +50 -0
- package/src/heatmap.ts +11 -0
- package/src/index.ts +11 -0
- package/src/legend-export.ts +36 -0
- package/src/legend-model.ts +76 -0
- package/src/legend.css +64 -0
- package/src/legend.ts +90 -0
- package/src/limits.ts +7 -0
- package/src/line.ts +13 -0
- package/src/loading-state.ts +39 -0
- package/src/map-assets.ts +1 -0
- package/src/map-resources.ts +46 -0
- package/src/messages.ts +5 -0
- package/src/model.ts +32 -0
- package/src/motion/bar.ts +15 -0
- package/src/motion/doughnut.ts +14 -0
- package/src/motion/heatmap.ts +14 -0
- package/src/motion/line.ts +15 -0
- package/src/motion/pie.ts +14 -0
- package/src/motion/radar.ts +15 -0
- package/src/motion/scatter.ts +15 -0
- package/src/motion/sparkline.ts +10 -0
- package/src/motion-collection.ts +20 -0
- package/src/motion-community.ts +22 -0
- package/src/motion-policy.ts +36 -0
- package/src/options.ts +24 -0
- package/src/pie.ts +12 -0
- package/src/radar.ts +12 -0
- package/src/runtime.ts +7 -0
- package/src/scatter.ts +13 -0
- package/src/sparkline-runtime/runtime.d.ts +10 -0
- package/src/sparkline-runtime/runtime.js +263 -0
- package/src/sparkline-validation.ts +19 -0
- package/src/sparkline.css +25 -0
- package/src/sparkline.ts +68 -0
- package/src/state-announcement.ts +17 -0
- package/src/types.ts +104 -0
- package/src/validation.ts +75 -0
- package/src/web-motion-sparkline.ts +37 -0
package/src/export.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { exportLegend, type LegendExportLayout } from './legend-export.ts';
|
|
2
|
+
import type { EChartsType } from 'echarts/core';
|
|
3
|
+
import type { ChartFormat, ChartOptions } from './types.ts';
|
|
4
|
+
import { escapeText } from './validation.ts';
|
|
5
|
+
import {boundedAwait} from './async.ts';
|
|
6
|
+
|
|
7
|
+
export function saveBlob(blob: Blob, filename: string) {
|
|
8
|
+
const url = URL.createObjectURL(blob);
|
|
9
|
+
const anchor = document.createElement('a');
|
|
10
|
+
anchor.href = url; anchor.download = filename;
|
|
11
|
+
document.body.append(anchor); anchor.click(); anchor.remove();
|
|
12
|
+
setTimeout(() => URL.revokeObjectURL(url), 1000);
|
|
13
|
+
}
|
|
14
|
+
function lines(text: string, max: number) {
|
|
15
|
+
const result: string[] = [];
|
|
16
|
+
for (const word of text.split(/\s+/)) {
|
|
17
|
+
if (!result.length || result[result.length - 1].length + word.length > max) result.push(word);
|
|
18
|
+
else result[result.length - 1] += ` ${word}`;
|
|
19
|
+
}
|
|
20
|
+
return result.slice(0, 4);
|
|
21
|
+
}
|
|
22
|
+
export async function exportImage(chart: EChartsType, options: ChartOptions, format: ChartFormat, filename: string, surface: string, text: string, muted: string, legend?: LegendExportLayout, signal?: AbortSignal) {
|
|
23
|
+
signal?.throwIfAborted();
|
|
24
|
+
const width = legend?.body.clientWidth || chart.getWidth();
|
|
25
|
+
const bodyHeight = legend?.body.getBoundingClientRect().height || chart.getHeight();
|
|
26
|
+
const origin = legend?.body.getBoundingClientRect(), plot = legend?.stage.getBoundingClientRect();
|
|
27
|
+
const plotX = origin && plot ? plot.x - origin.x : 0, plotY = origin && plot ? plot.y - origin.y : 0;
|
|
28
|
+
if (width <= 0 || chart.getHeight() <= 0) throw new Error('Show the chart before downloading it.');
|
|
29
|
+
const titleLines = lines(options.Title || 'Chart', Math.max(16, Math.floor((width - 32) / 10)));
|
|
30
|
+
const descriptionLines = lines(options.Description || '', Math.max(24, Math.floor((width - 32) / 6)));
|
|
31
|
+
const top = titleLines.length * 22 + descriptionLines.length * 15 + 24;
|
|
32
|
+
const height = bodyHeight + top + 32;
|
|
33
|
+
const title = titleLines.map((s, i) => `<text x="16" y="${26 + i * 22}" font-size="17" font-weight="600" fill="${text}">${escapeText(s)}</text>`).join('');
|
|
34
|
+
const subtitle = descriptionLines.map((s, i) => `<text x="16" y="${28 + titleLines.length * 22 + i * 15}" font-size="11" fill="${muted}">${escapeText(s)}</text>`).join('');
|
|
35
|
+
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}" role="img" aria-labelledby="chart-title chart-description"><title id="chart-title">${escapeText(options.Title || 'Chart')}</title><desc id="chart-description">${escapeText(options.Description || 'Chart visualization. Exact values are available in the source chart data table.')}</desc><rect width="100%" height="100%" fill="${surface}"/><g font-family="Inter,system-ui,sans-serif">${title}${subtitle}<g transform="translate(0 ${top})"><g transform="translate(${plotX} ${plotY})">${chart.renderToSVGString()}</g>${legend ? exportLegend(legend) : ''}</g><text x="16" y="${height - 12}" fill="${muted}" font-size="9">${escapeText(options.ExportFootnote || 'BluKavantUI')}</text></g></svg>`;
|
|
36
|
+
let blob = new Blob([svg], { type: 'image/svg+xml' });
|
|
37
|
+
if (format === 'png') {
|
|
38
|
+
if(width*2>16384||height*2>16384||width*height*4>32000000)throw new Error('This chart is too large for PNG. Use SVG or reduce the displayed data.');
|
|
39
|
+
const url = URL.createObjectURL(blob);
|
|
40
|
+
try {
|
|
41
|
+
const image = new Image(); image.src = url; await boundedAwait(image.decode(),signal,15000,'PNG decoding timed out. Try SVG.'); signal?.throwIfAborted();
|
|
42
|
+
const canvas = document.createElement('canvas'); canvas.width = Math.ceil(width * 2); canvas.height = Math.ceil(height * 2);
|
|
43
|
+
const context = canvas.getContext('2d');
|
|
44
|
+
if (!context) throw new Error('PNG export is unavailable. Try SVG.');
|
|
45
|
+
context.drawImage(image, 0, 0, canvas.width, canvas.height);
|
|
46
|
+
blob = await boundedAwait(new Promise<Blob>((resolve, reject) => canvas.toBlob(b => b ? resolve(b) : reject(new Error('PNG export failed. Try SVG.')), 'image/png')),signal,15000,'PNG encoding timed out. Try SVG.');
|
|
47
|
+
} finally { URL.revokeObjectURL(url); }
|
|
48
|
+
}
|
|
49
|
+
signal?.throwIfAborted(); saveBlob(blob, filename);
|
|
50
|
+
}
|
package/src/heatmap.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { heatmapOption as buildOption } from './builders/heatmap.ts';
|
|
2
|
+
import { use } from 'echarts/core';
|
|
3
|
+
import { HeatmapChart } from 'echarts/charts';
|
|
4
|
+
import { GridComponent, VisualMapComponent } from 'echarts/components';
|
|
5
|
+
import { createChart } from './runtime.ts';
|
|
6
|
+
import type { HeatmapOptions, HeatmapPoint, ChartEvents } from './types.ts';
|
|
7
|
+
export type { HeatmapOptions, HeatmapCell, HeatmapPoint, ChartCategory, ChartEvents, ChartInstance } from './types.ts';
|
|
8
|
+
export function createHeatmap(host: HTMLElement, options: HeatmapOptions, events: ChartEvents<HeatmapPoint> = {}) {
|
|
9
|
+
use([HeatmapChart, GridComponent, VisualMapComponent]);
|
|
10
|
+
return createChart<HeatmapOptions, HeatmapPoint>(host, 'heatmap', options, events, buildOption);
|
|
11
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export type * from './types.ts';
|
|
2
|
+
export type * from './catalogue-types.ts';
|
|
3
|
+
export { createLineChart } from './line.ts';
|
|
4
|
+
export { createBarChart } from './bar.ts';
|
|
5
|
+
export { createPieChart } from './pie.ts';
|
|
6
|
+
export { createDoughnutChart } from './doughnut.ts';
|
|
7
|
+
export { createScatterChart } from './scatter.ts';
|
|
8
|
+
export { createRadarChart } from './radar.ts';
|
|
9
|
+
export { createHeatmap } from './heatmap.ts';
|
|
10
|
+
export { createSparkline } from './sparkline.ts';
|
|
11
|
+
export type { BluSparklinePoint, SparklineOptions, SparklineEvents, SparklineInstance } from './sparkline.ts';
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { LegendModel } from './legend-model.ts';
|
|
2
|
+
import { escapeText as esc } from './validation.ts';
|
|
3
|
+
export interface LegendExportLayout { body: HTMLElement; stage: HTMLElement; legend: HTMLElement; model: LegendModel }
|
|
4
|
+
/** Export the shared HTML key as native SVG, including wrapped labels; no foreignObject. */
|
|
5
|
+
export function exportLegend(layout: LegendExportLayout): string {
|
|
6
|
+
const {body,legend,model}=layout;
|
|
7
|
+
if(legend.hidden) return '';
|
|
8
|
+
const origin=body.getBoundingClientRect(), parts: string[]=[];
|
|
9
|
+
const rect = (el: Element) => {const b=el.getBoundingClientRect();return {x:b.x-origin.x,y:b.y-origin.y,w:b.width,h:b.height};};
|
|
10
|
+
for(const el of legend.querySelectorAll<HTMLElement>('.blu-chart__legend-entry,.blu-chart__legend-chip,.blu-chart__legend-scale')) {
|
|
11
|
+
const r=rect(el), s=getComputedStyle(el);
|
|
12
|
+
parts.push(`<rect x="${r.x}" y="${r.y}" width="${r.w}" height="${r.h}" rx="${Math.min(parseFloat(s.borderRadius)||0,r.w/2,r.h/2)}" fill="${esc(s.backgroundColor)}" stroke="${parseFloat(s.borderWidth)>0?esc(s.borderColor):'none'}" stroke-width="${parseFloat(s.borderWidth)||0}"/>`);
|
|
13
|
+
}
|
|
14
|
+
for(const el of legend.querySelectorAll<HTMLElement>('.blu-chart__legend-marker svg')) {
|
|
15
|
+
const r=rect(el); parts.push(`<svg x="${r.x}" y="${r.y}" width="${r.w}" height="${r.h}" viewBox="${esc(el.getAttribute('viewBox') || '0 0 28 16')}">${el.innerHTML}</svg>`);
|
|
16
|
+
}
|
|
17
|
+
const gradient=legend.querySelector('.blu-chart__legend-gradient');
|
|
18
|
+
if(gradient && model.scale) {
|
|
19
|
+
const r=rect(gradient);
|
|
20
|
+
parts.push(`<defs><linearGradient id="blu-export-legend-scale">${model.scale.colors.map((c,i)=>`<stop offset="${i/(model.scale!.colors.length-1)*100}%" stop-color="${esc(c)}"/>`).join('')}</linearGradient></defs><rect x="${r.x}" y="${r.y}" width="${r.w}" height="${r.h}" rx="4" fill="url(#blu-export-legend-scale)"/>`);
|
|
21
|
+
}
|
|
22
|
+
for(const el of legend.querySelectorAll<HTMLElement>('.blu-chart__legend-track,.blu-chart__legend-track>span')) {
|
|
23
|
+
const r=rect(el); if(r.w && r.h) parts.push(`<rect x="${r.x}" y="${r.y}" width="${r.w}" height="${r.h}" rx="2" fill="${esc(getComputedStyle(el).backgroundColor)}"/>`);
|
|
24
|
+
}
|
|
25
|
+
for(const el of legend.querySelectorAll<HTMLElement>('.blu-chart__legend-text')) {
|
|
26
|
+
const node=el.firstChild; if(!node || node.nodeType!==Node.TEXT_NODE) continue;
|
|
27
|
+
const s=getComputedStyle(el); if(!el.getClientRects().length || s.visibility==='hidden') continue;
|
|
28
|
+
const font=parseFloat(s.fontSize), range=document.createRange(), rows:{x:number;y:number;text:string}[]=[];
|
|
29
|
+
for(let i=0;i<node.textContent!.length;i++) {
|
|
30
|
+
range.setStart(node,i);range.setEnd(node,i+1);const r=range.getBoundingClientRect();
|
|
31
|
+
const last=rows.at(-1);if(last && Math.abs(last.y-r.y)<1) last.text+=node.textContent![i]; else rows.push({x:r.x,y:r.y,text:node.textContent![i]});
|
|
32
|
+
}
|
|
33
|
+
for(const row of rows) parts.push(`<text x="${row.x-origin.x}" y="${row.y-origin.y+font*.86}" font-size="${font}" font-weight="${s.fontWeight}" fill="${esc(s.color)}"${s.textDecorationLine.includes('underline')?' text-decoration="underline"':''}>${esc(row.text)}</text>`);
|
|
34
|
+
}
|
|
35
|
+
return parts.join('');
|
|
36
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { ChartOptions } from './types.ts';
|
|
2
|
+
import type { ChartTheme } from './options.ts';
|
|
3
|
+
import {chartMessage} from './messages.ts';
|
|
4
|
+
import { formatValue } from './model.ts';
|
|
5
|
+
import { treeValue, treeTotals } from './catalogue-model.ts';
|
|
6
|
+
import { escapeText } from './validation.ts';
|
|
7
|
+
|
|
8
|
+
export interface LegendEntry {
|
|
9
|
+
id: string; label: string; color: string; value?: string; compactValue?: string; detail?: string; percent?: number;
|
|
10
|
+
symbol?: string; dash?: string; line?: boolean;
|
|
11
|
+
targets: { seriesIndex: number; dataIndex?: number; name?: string }[];
|
|
12
|
+
}
|
|
13
|
+
export interface LegendScale { label: string; min: number; max: number; colors: string[] }
|
|
14
|
+
export interface LegendModel { entries: LegendEntry[]; scale?: LegendScale; note?: string }
|
|
15
|
+
/** Adapt data once; every chart uses the same DOM renderer and export path. */
|
|
16
|
+
export function legendModel(kind: string, o: any, config: any, t: ChartTheme): LegendModel {
|
|
17
|
+
const series: any[] = config.series || [], entries: LegendEntry[] = [];
|
|
18
|
+
const msg=(key:string,fallback:string,values:Record<string,string>={})=>chartMessage(o,key,fallback,values);
|
|
19
|
+
const dataColor = (d: any, s: any) => d?.itemStyle?.color || d?.itemStyle?.areaColor || s?.itemStyle?.color || s?.lineStyle?.color || t.palette[0];
|
|
20
|
+
const addItem = (d: any, s: any, si: number, di: number, label: string, id: string, value: number | null, detail?: string, percent?: number) => entries.push({ id, label, color: dataColor(d,s), value: formatValue(value,o), compactValue: formatValue(value,o,true), detail, percent, targets: [{seriesIndex:si,dataIndex:di}] });
|
|
21
|
+
if (o.Series) {
|
|
22
|
+
o.Series.forEach((s: any, si: number) => {
|
|
23
|
+
const rendered = series[si], present = s.Values.flatMap((v: number | null, i: number) => v === null ? [] : [{v,i}]), last = present.at(-1);
|
|
24
|
+
const radar = kind === 'radar';
|
|
25
|
+
entries.push({ id: s.Key, label: s.Label || s.Name, color: rendered?.lineStyle?.color || dataColor(null, rendered),
|
|
26
|
+
line: rendered?.type === 'line' || radar, symbol: rendered?.symbol, dash: rendered?.lineStyle?.type,
|
|
27
|
+
value: radar ? `${present.length} / ${o.Categories.length}` : formatValue(last?.v ?? null,o),
|
|
28
|
+
compactValue: radar ? undefined : formatValue(last?.v ?? null,o,true),
|
|
29
|
+
detail: radar ? msg('MeasuresReported','Measures reported') : last ? msg('Latest','Latest · {label}',{label:o.Categories[last.i].Label}) : msg('LatestValue','Latest value'), targets: [{seriesIndex:si}] });
|
|
30
|
+
});
|
|
31
|
+
} else if (['pie','doughnut','nightingale','funnel'].includes(kind)) {
|
|
32
|
+
series.forEach((s,si) => {
|
|
33
|
+
const total = s.data.reduce((n: number,d: any) => n + (d.value ?? 0),0), ring = o.Nested ? o.Rings?.[si]?.Label : undefined;
|
|
34
|
+
s.data.forEach((d: any,di: number) => addItem(d,s,si,di,d.name,d.point.Key,d.value,ring,
|
|
35
|
+
kind !== 'funnel' && d.value !== null && total > 0 ? d.value / total : undefined));
|
|
36
|
+
});
|
|
37
|
+
} else if (kind === 'scatter') {
|
|
38
|
+
series.forEach((s,si) => entries.push({id:s.name,label:s.name,color:dataColor(null,s),symbol:s.symbol,value:String(s.data.length),detail:msg('Observations','Observations'),targets:[{seriesIndex:si}]}));
|
|
39
|
+
} else if (kind === 'treemap') {
|
|
40
|
+
const totals = treeTotals(o.Nodes);
|
|
41
|
+
const roots = o.Nodes.filter((n: any) => n.ParentKey === null), total = roots.reduce((n: number,r: any) => n + (totals.get(r.Key) ?? 0),0);
|
|
42
|
+
// ECharts includes a virtual root at index 0, then flattens nodes depth-first.
|
|
43
|
+
const indices = new Map<string,number>(); let index = 1;
|
|
44
|
+
const walk = (nodes: any[]) => nodes.forEach(n => { indices.set(n.id,index++); walk(n.children || []); });
|
|
45
|
+
walk(series[0].data);
|
|
46
|
+
roots.forEach((n: any,di: number) => { const value = totals.get(n.Key)!; addItem(series[0].data[di],series[0],0,indices.get(n.Key)!,n.Label,n.Key,value,msg('TopLevelGroup','Top-level group'),total > 0 && value !== null ? value/total : undefined); });
|
|
47
|
+
} else if (kind === 'sankey') {
|
|
48
|
+
o.Nodes.forEach((n: any,di: number) => {
|
|
49
|
+
const incoming = o.Links.filter((l: any)=>l.TargetKey===n.Key).reduce((v: number,l: any)=>v+l.Value,0);
|
|
50
|
+
const outgoing = o.Links.filter((l: any)=>l.SourceKey===n.Key).reduce((v: number,l: any)=>v+l.Value,0);
|
|
51
|
+
addItem(series[0].data[di],series[0],0,di,n.Label,n.Key,Math.max(incoming,outgoing),msg('NodeThroughput','Node throughput'));
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
const vm = Array.isArray(config.visualMap) ? config.visualMap[0] : config.visualMap;
|
|
55
|
+
const scale = vm ? { label: kind === 'geo-map' ? msg('RegionValue','Region value') : kind === 'calendar-heatmap' ? msg('DailyValue','Daily value') : msg('CellValue','Cell value'), min: vm.min, max: vm.max, colors: [...vm.inRange.color] } : undefined;
|
|
56
|
+
if (kind === 'geo-map') {
|
|
57
|
+
series.forEach((s,si) => s.data.forEach((d: any,di: number) => {
|
|
58
|
+
if(s.type === 'scatter' || o.Regions.find((r: any)=>r.Key===d.point.Key)?.Color)
|
|
59
|
+
addItem(d,s,si,di,d.point.Label,d.point.Key,d.point.Value,s.type==='scatter'?msg('LocationValue','Location · size = value'):msg('CustomRegionColor','Custom region color'));
|
|
60
|
+
}));
|
|
61
|
+
} else if (kind === 'calendar-heatmap') {
|
|
62
|
+
series[0]?.data.forEach((d: any,di: number) => { if(o.Items.find((i: any)=>i.Key===d.point.Key)?.Color) addItem(d,series[0],0,di,d.point.Label,d.point.Key,d.point.Value,msg('CustomDayColor','Custom day color')); });
|
|
63
|
+
}
|
|
64
|
+
return { entries, scale, note: kind === 'scatter' && o.Bubble ? msg('BubbleSize','Bubble size represents Size.') : undefined };
|
|
65
|
+
}
|
|
66
|
+
/** No percentages for overlapping series, funnel stages, flows or unrelated axes. */
|
|
67
|
+
export function legendText(e: LegendEntry, o: ChartOptions) {
|
|
68
|
+
return { value: o.LegendShowValues === false ? '' : e.compactValue || e.value || '', detail: o.LegendShowValues === false ? '' : e.detail || '',
|
|
69
|
+
percent: o.LegendShowPercentages && e.percent !== undefined ? new Intl.NumberFormat(o.Locale || 'en-US',{style:'percent',maximumFractionDigits:1}).format(e.percent) : '' };
|
|
70
|
+
}
|
|
71
|
+
export function legendMarker(e: LegendEntry) {
|
|
72
|
+
const color = escapeText(e.color), symbol = e.symbol || 'rect', hollow = symbol.startsWith('empty'), shape = symbol.replace(/^empty/,'').toLowerCase();
|
|
73
|
+
const fill = hollow ? 'none' : color, dash = e.dash === 'dashed' ? '5 3' : e.dash === 'dotted' ? '2 3' : '';
|
|
74
|
+
const mark = shape.includes('diamond') ? '<path d="M14 2 20 8 14 14 8 8Z"' : shape.includes('triangle') || shape === 'arrow' ? '<path d="M14 2 20 13 8 13Z"' : shape.includes('circle') || shape === 'pin' ? '<circle cx="14" cy="8" r="4"' : '<rect x="9" y="3" width="10" height="10" rx="2"';
|
|
75
|
+
return `<svg viewBox="${e.line ? '0 0 28 16' : '6 0 16 16'}" width="${e.line ? 28 : 16}" height="16" aria-hidden="true">${e.line ? `<path d="M1 8H27" fill="none" stroke="${color}" stroke-width="2" stroke-dasharray="${dash}"/>` : ''}${mark} fill="${fill}" stroke="${color}" stroke-width="1.5"/></svg>`;
|
|
76
|
+
}
|
package/src/legend.css
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
.blu-chart__body {display:grid;grid-template-columns:minmax(0,1fr);gap:12px;min-width:0}
|
|
2
|
+
.blu-chart__body[data-legend-position=Top] .blu-chart__legend {grid-row:1}
|
|
3
|
+
.blu-chart__body[data-legend-position=Bottom] .blu-chart__legend {grid-row:2}
|
|
4
|
+
.blu-chart__body[data-legend-position=Left] {grid-template-columns:190px minmax(0,1fr);align-items:center}
|
|
5
|
+
.blu-chart__body[data-legend-position=Right] {grid-template-columns:minmax(0,1fr) 190px;align-items:center}
|
|
6
|
+
.blu-chart__body[data-legend-position=Left] .blu-chart__legend {grid-column:1;grid-row:1}
|
|
7
|
+
.blu-chart__body[data-legend-position=Left] .blu-chart__stage {grid-column:2;grid-row:1}
|
|
8
|
+
.blu-chart__body[data-legend-position=Right] .blu-chart__legend {grid-column:2;grid-row:1}
|
|
9
|
+
.blu-chart__body[data-legend-position=Right] .blu-chart__stage {grid-column:1;grid-row:1}
|
|
10
|
+
.blu-chart__body.blu-chart__body--no-legend {grid-template-columns:minmax(0,1fr);gap:0}
|
|
11
|
+
.blu-chart__body--no-legend .blu-chart__stage {grid-column:1!important;grid-row:1!important}
|
|
12
|
+
.blu-chart__legend {--legend-selected:color-mix(in srgb,var(--bluui-primary) 9%,var(--bluui-surface));min-width:0;padding:8px 20px 16px;font:12px/1.45 var(--bluui-font,Inter,system-ui,sans-serif);color:var(--bluui-text)}
|
|
13
|
+
.blu-chart__legend-items {display:flex;flex-wrap:wrap;gap:8px}
|
|
14
|
+
.blu-chart__legend-entry {min-width:0;max-width:100%;display:flex;align-items:center;gap:10px;border:1px solid transparent;background:transparent;color:inherit;font:inherit;text-align:left;padding:7px 8px;border-radius:8px;cursor:pointer;min-height:36px}
|
|
15
|
+
.blu-chart__legend-entry:hover {background:var(--bluui-surface-muted)}
|
|
16
|
+
.blu-chart__legend-entry[aria-pressed=true] {text-decoration:none;background:var(--legend-selected)}
|
|
17
|
+
.blu-chart__legend-heading {display:flex;align-items:center;gap:7px;min-width:0}
|
|
18
|
+
.blu-chart__legend-marker {display:flex;flex-shrink:0}
|
|
19
|
+
.blu-chart__legend-text {overflow-wrap:anywhere}
|
|
20
|
+
.blu-chart__legend-label {font-weight:500}
|
|
21
|
+
.blu-chart__legend-detail,.blu-chart__legend-percent,.blu-chart__legend-note {font-size:11px;color:var(--bluui-text-muted)}
|
|
22
|
+
.blu-chart__legend-numbers {display:flex;align-items:baseline;gap:10px;min-width:0;font-variant-numeric:tabular-nums}
|
|
23
|
+
.blu-chart__legend-value {font-weight:600;letter-spacing:-.25px}
|
|
24
|
+
.blu-chart__legend-check {visibility:hidden;margin-left:auto;color:var(--bluui-primary);font-weight:700;font-size:12px}
|
|
25
|
+
[aria-pressed=true] .blu-chart__legend-check {visibility:visible}
|
|
26
|
+
.blu-chart__legend[data-style=Standard] .blu-chart__legend-detail,.blu-chart__legend[data-style=Standard] .blu-chart__legend-check {display:none}
|
|
27
|
+
.blu-chart__legend[data-style=Standard] .blu-chart__legend-value {font-size:11px;color:var(--bluui-text-muted)}
|
|
28
|
+
.blu-chart__legend-track {display:none;height:3px;background:var(--bluui-border);border-radius:2px;overflow:hidden}
|
|
29
|
+
.blu-chart__legend-track>span {display:block;height:100%}
|
|
30
|
+
/* The compact key uses Community Chip; only this chart-specific appearance is scoped here. */
|
|
31
|
+
.blu-chart__legend-chip-host {min-width:0;max-width:100%}
|
|
32
|
+
.blu-chart__legend .blu-chip.blu-chart__legend-chip {--chip-bg:transparent;--chip-fg:var(--bluui-text);--chip-border:var(--bluui-border);max-width:100%;box-shadow:none}
|
|
33
|
+
.blu-chart__legend .blu-chart__legend-chip.blu-chip--selected {--chip-bg:var(--legend-selected);--chip-border:var(--bluui-primary);box-shadow:none}
|
|
34
|
+
.blu-chart__legend .blu-chart__legend-chip .blu-chip__select {min-height:38px;padding:9px 12px;gap:10px;font:inherit;font-size:11px;text-align:left;justify-content:flex-start}
|
|
35
|
+
.blu-chart__legend-chip .blu-chart__legend-label {font-weight:500}
|
|
36
|
+
.blu-chart__legend-chip .blu-chart__legend-detail,.blu-chart__legend-chip .blu-chart__legend-check {display:none}
|
|
37
|
+
.blu-chart__legend-chip [aria-pressed=true] {text-decoration:none}
|
|
38
|
+
.blu-chart__legend-chip [aria-pressed=true] .blu-chart__legend-label {text-decoration:underline;text-underline-offset:3px}
|
|
39
|
+
.blu-chart__legend-chip .blu-chart__legend-numbers {gap:7px;flex-wrap:wrap}
|
|
40
|
+
.blu-chart__legend[data-style=ValueList] .blu-chart__legend-items,.blu-chart__legend[data-style=SummaryCards] .blu-chart__legend-items {display:grid;grid-template-columns:repeat(var(--legend-columns,1),minmax(0,1fr));gap:10px}
|
|
41
|
+
.blu-chart__legend[data-style=ValueList] .blu-chart__legend-entry {display:flex;flex-direction:column;align-items:stretch;gap:5px;padding:10px 11px;border:0}
|
|
42
|
+
.blu-chart__legend[data-style=ValueList] .blu-chart__legend-numbers {margin-left:23px;justify-content:space-between}
|
|
43
|
+
.blu-chart__legend[data-style=ValueList] .blu-chart__legend-value {font-size:19px;letter-spacing:-.4px;line-height:1.3}
|
|
44
|
+
.blu-chart__legend[data-style=ValueList] .blu-chart__legend-detail {margin-left:23px}
|
|
45
|
+
.blu-chart__legend[data-style=ValueList] .blu-chart__legend-track {display:block;margin:3px 0 0 23px}
|
|
46
|
+
.blu-chart__legend[data-style=SummaryCards] .blu-chart__legend-entry {display:flex;flex-direction:column;align-items:stretch;justify-content:flex-start;gap:0;padding:12px;min-height:36px;align-self:start;border-color:var(--bluui-border);border-radius:12px}
|
|
47
|
+
.blu-chart__legend[data-style=SummaryCards] .blu-chart__legend-entry[aria-pressed=true] {border-color:var(--bluui-primary)}
|
|
48
|
+
.blu-chart__legend[data-style=SummaryCards] .blu-chart__legend-heading {font-size:11px}
|
|
49
|
+
.blu-chart__legend[data-style=SummaryCards] .blu-chart__legend-numbers {flex-direction:column;align-items:stretch;gap:6px;margin-top:10px}
|
|
50
|
+
.blu-chart__legend[data-style=SummaryCards] .blu-chart__legend-value {font-size:24px;letter-spacing:-.6px;line-height:1.2}
|
|
51
|
+
.blu-chart__legend[data-style=SummaryCards] .blu-chart__legend-detail {margin-top:6px}
|
|
52
|
+
.blu-chart__body:is([data-legend-position=Left],[data-legend-position=Right]) .blu-chart__legend {padding:8px 12px 16px}
|
|
53
|
+
.blu-chart__body:is([data-legend-position=Left],[data-legend-position=Right]) .blu-chart__legend-items {display:flex;flex-direction:column}
|
|
54
|
+
.blu-chart__body:is([data-legend-position=Left],[data-legend-position=Right]) .blu-chart__legend-entry {width:100%}
|
|
55
|
+
.blu-chart__body:is([data-legend-position=Left],[data-legend-position=Right]) .blu-chart__legend[data-style=Standard] .blu-chart__legend-entry {flex-wrap:wrap;gap:4px 10px}
|
|
56
|
+
.blu-chart__legend-scale {max-width:360px;min-width:0;margin:0 0 8px}
|
|
57
|
+
.blu-chart__legend-scale-title {display:block;font-size:11px;font-weight:500;margin-bottom:10px;color:var(--bluui-text-muted)}
|
|
58
|
+
.blu-chart__legend-gradient {height:8px;border-radius:4px}
|
|
59
|
+
.blu-chart__legend-range {display:flex;justify-content:space-between;gap:12px;margin-top:8px;font-variant-numeric:tabular-nums}
|
|
60
|
+
.blu-chart__legend[data-style=Compact] .blu-chart__legend-scale {padding:12px 16px;border:1px solid var(--bluui-border);border-radius:18px}
|
|
61
|
+
.blu-chart__legend[data-style=ValueList] .blu-chart__legend-scale {padding:10px 11px}
|
|
62
|
+
.blu-chart__legend[data-style=SummaryCards] .blu-chart__legend-scale {padding:16px;border:1px solid var(--bluui-border);border-radius:12px}
|
|
63
|
+
.blu-chart__legend-note {display:block;margin-top:10px}
|
|
64
|
+
@media(forced-colors:active) {.blu-chart__legend-entry{border:1px solid ButtonText!important}.blu-chart__legend-entry[aria-pressed=true],.blu-chart__legend .blu-chart__legend-chip.blu-chip--selected{outline:1px solid Highlight}.blu-chart__legend-marker svg{forced-color-adjust:none}.blu-chart__legend-gradient{background:Canvas!important;border:1px solid CanvasText}}
|
package/src/legend.ts
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import type { EChartsType } from 'echarts/core';
|
|
2
|
+
import { createChip } from '@blukavantui/community/chip';
|
|
3
|
+
import '@blukavantui/community/chip.css';
|
|
4
|
+
import type { ChartOptions } from './types.ts';
|
|
5
|
+
import type { ChartTheme } from './options.ts';
|
|
6
|
+
import { legendModel, legendText, legendMarker, type LegendModel, type LegendEntry } from './legend-model.ts';
|
|
7
|
+
import { formatValue } from './model.ts';
|
|
8
|
+
import { escapeText as esc } from './validation.ts';
|
|
9
|
+
import './legend.css';
|
|
10
|
+
import {chartMessage} from './messages.ts';
|
|
11
|
+
|
|
12
|
+
export function createChartLegend(root: HTMLElement, chart: EChartsType, kind: string) {
|
|
13
|
+
const stage = root.querySelector<HTMLElement>('.blu-chart__stage')!;
|
|
14
|
+
const body = document.createElement('div'); body.className = 'blu-chart__body';
|
|
15
|
+
stage.before(body); body.append(stage);
|
|
16
|
+
const host = document.createElement('div'); host.className = 'blu-chart__legend'; host.setAttribute('role','group'); host.setAttribute('aria-label','Chart legend');
|
|
17
|
+
body.append(host);
|
|
18
|
+
let focused: string | null = null, model: LegendModel = {entries:[]}, options: ChartOptions = {}, signature = '';
|
|
19
|
+
const chips: {entry: LegendEntry; host: HTMLElement; instance: ReturnType<typeof createChip>}[] = [];
|
|
20
|
+
const status = root.querySelector<HTMLElement>('.blu-chart__status')!;
|
|
21
|
+
function content(e: LegendEntry) {
|
|
22
|
+
const info=legendText(e,options);
|
|
23
|
+
return `<span class="blu-chart__legend-heading"><span class="blu-chart__legend-marker">${legendMarker(e)}</span><span class="blu-chart__legend-text blu-chart__legend-label">${esc(e.label)}</span><span class="blu-chart__legend-text blu-chart__legend-check" aria-hidden="true">✓</span></span>${info.value||info.percent?`<span class="blu-chart__legend-numbers">${info.value?`<span class="blu-chart__legend-text blu-chart__legend-value">${esc(info.value)}</span>`:''}${info.percent?`<span class="blu-chart__legend-text blu-chart__legend-percent">${esc(info.percent)}</span>`:''}</span>`:''}${info.detail?`<span class="blu-chart__legend-text blu-chart__legend-detail">${esc(info.detail)}</span>`:''}${options.LegendStyle==='ValueList'?`<span class="blu-chart__legend-track" aria-hidden="true"><span style="width:${e.percent===undefined?100:Math.max(0,Math.min(100,e.percent*100))}%;background:${esc(e.color)}"></span></span>`:''}`;
|
|
24
|
+
}
|
|
25
|
+
function decorate(button: HTMLButtonElement, e: LegendEntry) {
|
|
26
|
+
const info=legendText(e,options);
|
|
27
|
+
button.dataset.legendKey=e.id;
|
|
28
|
+
button.setAttribute('aria-label',`${chartMessage(options,'Highlight','Highlight {label}',{label:e.label})}${info.detail?' · '+info.detail:''}${info.value?': '+e.value:''}${info.percent?' · '+info.percent:''}`);
|
|
29
|
+
button.innerHTML=content(e);
|
|
30
|
+
}
|
|
31
|
+
function emphasis() {
|
|
32
|
+
chart.dispatchAction({type:'downplay'});
|
|
33
|
+
const entry = model.entries.find(e=>e.id===focused);
|
|
34
|
+
entry?.targets.forEach(target=>chart.dispatchAction({type:'highlight',...target}));
|
|
35
|
+
// Chip owns its selection semantics; reapply the composed marker after its render.
|
|
36
|
+
chips.forEach(c=>{ c.instance.update({IsSelected:c.entry.id===focused}); decorate(c.host.querySelector<HTMLButtonElement>('button')!,c.entry); });
|
|
37
|
+
host.querySelectorAll<HTMLElement>('[data-legend-key]').forEach(b=>b.setAttribute('aria-pressed',String(b.dataset.legendKey===focused)));
|
|
38
|
+
}
|
|
39
|
+
function activate(id: string) {
|
|
40
|
+
focused=focused===id ? null : id;
|
|
41
|
+
emphasis();
|
|
42
|
+
status.textContent=focused ? chartMessage(options,'Highlighting','Highlighting {label}. Activate again to clear.',{label:model.entries.find(e=>e.id===focused)!.label}) : chartMessage(options,'HighlightCleared','Legend highlight cleared.');
|
|
43
|
+
}
|
|
44
|
+
function disposeChips() { chips.splice(0).forEach(c=>c.instance.destroy()); }
|
|
45
|
+
return {
|
|
46
|
+
/** Place the legend before measuring the plot. Side positions reflow in narrow chart cards. */
|
|
47
|
+
prepare(o: ChartOptions, unavailable: boolean) {
|
|
48
|
+
options=o;
|
|
49
|
+
host.setAttribute('aria-label',chartMessage(o,'Legend','Chart legend'));
|
|
50
|
+
const requested=o.LegendPosition || 'Top';
|
|
51
|
+
const position = root.clientWidth < 520 && ['Left','Right'].includes(requested) ? 'Bottom' : requested;
|
|
52
|
+
body.dataset.legendPosition=position;
|
|
53
|
+
host.hidden=o.ShowLegend===false || unavailable;
|
|
54
|
+
body.classList.toggle('blu-chart__body--no-legend',host.hidden);
|
|
55
|
+
host.dataset.style=o.LegendStyle || 'Standard';
|
|
56
|
+
if(host.hidden) focused=null;
|
|
57
|
+
},
|
|
58
|
+
render(o: any, config: any, theme: ChartTheme) {
|
|
59
|
+
model=legendModel(kind,o,config,theme);
|
|
60
|
+
if(!model.entries.some(e=>e.id===focused)) focused=null;
|
|
61
|
+
if(!model.entries.length && !model.scale) {host.hidden=true;body.classList.add('blu-chart__body--no-legend');}
|
|
62
|
+
const capacity=Math.max(1,Math.min(4,Math.floor((host.clientWidth-30)/150)));
|
|
63
|
+
const columns=Math.ceil(model.entries.length/Math.ceil(model.entries.length/capacity)) || 1;
|
|
64
|
+
host.style.setProperty('--legend-columns',String(columns));
|
|
65
|
+
const next=JSON.stringify([model,o.LegendStyle,o.LegendShowValues,o.LegendShowPercentages,o.Messages,theme.surface,theme.text]);
|
|
66
|
+
if(next !== signature) {
|
|
67
|
+
const active = host.contains(document.activeElement) ? (document.activeElement as HTMLElement).dataset.legendKey : undefined;
|
|
68
|
+
signature=next;
|
|
69
|
+
disposeChips();
|
|
70
|
+
host.innerHTML=`${model.scale ? `<div class="blu-chart__legend-scale"><span class="blu-chart__legend-text blu-chart__legend-scale-title">${esc(model.scale.label)}</span><div class="blu-chart__legend-gradient" style="background:linear-gradient(90deg,${model.scale.colors.map(esc).join(',')})" aria-hidden="true"></div><div class="blu-chart__legend-range"><span class="blu-chart__legend-text">${esc(formatValue(model.scale.min,o))}</span><span class="blu-chart__legend-text">${esc(formatValue(model.scale.max,o))}</span></div></div>` : ''}<div class="blu-chart__legend-items"></div>${model.note ? `<span class="blu-chart__legend-text blu-chart__legend-note">${esc(model.note)}</span>` : ''}`;
|
|
71
|
+
const items=host.querySelector<HTMLElement>('.blu-chart__legend-items')!;
|
|
72
|
+
model.entries.forEach(e=>{
|
|
73
|
+
if(o.LegendStyle==='Compact') {
|
|
74
|
+
const chipHost=document.createElement('div'); chipHost.className='blu-chart__legend-chip-host'; items.append(chipHost);
|
|
75
|
+
const instance=createChip(chipHost,{Label:e.label,Color:'Secondary',IsSelectable:true,IsSelected:e.id===focused,ExtendedClass:'blu-chart__legend-chip'}, {SelectionChanged:()=>activate(e.id)});
|
|
76
|
+
chips.push({entry:e,host:chipHost,instance});
|
|
77
|
+
} else {
|
|
78
|
+
const button=document.createElement('button'); button.type='button'; button.className='blu-chart__legend-entry';
|
|
79
|
+
decorate(button,e); button.onclick=()=>activate(e.id); items.append(button);
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
emphasis();
|
|
83
|
+
if(active) [...host.querySelectorAll<HTMLElement>('[data-legend-key]')].find(b=>b.dataset.legendKey===active)?.focus({preventScroll:true});
|
|
84
|
+
} else emphasis();
|
|
85
|
+
},
|
|
86
|
+
exportLayout() { return {body,stage,legend:host,model}; },
|
|
87
|
+
reset() {focused=null;},
|
|
88
|
+
destroy() {disposeChips();host.replaceChildren();},
|
|
89
|
+
};
|
|
90
|
+
}
|
package/src/limits.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** Defensive input limits, not a rendering-speed guarantee. Aggregate larger inputs upstream. */
|
|
2
|
+
export const limits = Object.freeze({records: 10000, observations: 10000, treeNodes: 2000, treeDepth: 64, flowNodes: 1000, flowLinks: 5000, mapFeatures: 5000, mapCoordinates: 250000, mapBytes: 8000000, text: 10000});
|
|
3
|
+
export function safeNumber(n: unknown): n is number { return typeof n === 'number' && Number.isFinite(n) && Math.abs(n) <= Number.MAX_SAFE_INTEGER; }
|
|
4
|
+
export function boundedList(v: unknown, name: string, max: number = limits.records): asserts v is any[] {
|
|
5
|
+
if (!Array.isArray(v) || v.length > max) throw new Error(`${name} must be a list with at most ${max} records.`);
|
|
6
|
+
}
|
|
7
|
+
export function boundedText(v: unknown): v is string { return typeof v === 'string' && v.length <= limits.text; }
|
package/src/line.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { buildOption } from './builders/line.ts';
|
|
2
|
+
import { use } from 'echarts/core';
|
|
3
|
+
import { LineChart } from 'echarts/charts';
|
|
4
|
+
import { GridComponent } from 'echarts/components';
|
|
5
|
+
import { createCatalogueChart } from './catalogue-runtime.ts';
|
|
6
|
+
import type { OptionsByKind, ChartPoint } from './catalogue-types.ts';
|
|
7
|
+
import type { ChartEvents } from './types.ts';
|
|
8
|
+
export type { OptionsByKind, ChartPoint } from './catalogue-types.ts';
|
|
9
|
+
export type LineChartOptions = OptionsByKind['line'];
|
|
10
|
+
export function createLineChart(host: HTMLElement, options: LineChartOptions, events: ChartEvents<ChartPoint> = {}) {
|
|
11
|
+
use([LineChart, GridComponent]);
|
|
12
|
+
return createCatalogueChart(host, 'line', options, events, buildOption);
|
|
13
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { createSkeleton } from '@blukavantui/community/skeleton';
|
|
2
|
+
import '@blukavantui/community/skeleton.css';
|
|
3
|
+
|
|
4
|
+
/** Decorative placeholders; the chart's separate status announces state changes. */
|
|
5
|
+
export function chartLoadingState(host: HTMLElement) {
|
|
6
|
+
const shapes = document.createElement('div');
|
|
7
|
+
shapes.className = 'blu-chart__skeleton';
|
|
8
|
+
shapes.setAttribute('aria-hidden', 'true');
|
|
9
|
+
const key = document.createElement('div');
|
|
10
|
+
key.className = 'blu-chart__skeleton-key';
|
|
11
|
+
const first = document.createElement('div'), second = document.createElement('div');
|
|
12
|
+
key.append(first, second);
|
|
13
|
+
const plot = document.createElement('div');
|
|
14
|
+
plot.className = 'blu-chart__skeleton-plot';
|
|
15
|
+
shapes.append(key, plot);
|
|
16
|
+
const message = document.createElement('span');
|
|
17
|
+
message.className = 'blu-chart__state-message';
|
|
18
|
+
host.replaceChildren(shapes, message);
|
|
19
|
+
const skeletons = [
|
|
20
|
+
createSkeleton(first, { Shape: 'Text', Width: '88px', Height: '12px', IsAnimated: false }),
|
|
21
|
+
createSkeleton(second, { Shape: 'Text', Width: '64px', Height: '12px', IsAnimated: false }),
|
|
22
|
+
createSkeleton(plot, { Shape: 'Rectangle', Width: '100%', Height: '100%', IsAnimated: false }),
|
|
23
|
+
];
|
|
24
|
+
let animated = false;
|
|
25
|
+
return {
|
|
26
|
+
update(busy: boolean, empty: boolean, label: string, allowMotion: boolean) {
|
|
27
|
+
host.hidden = !busy && !empty;
|
|
28
|
+
host.classList.toggle('blu-chart__state--loading', busy);
|
|
29
|
+
shapes.hidden = !busy;
|
|
30
|
+
message.textContent = label;
|
|
31
|
+
const next = busy && allowMotion;
|
|
32
|
+
if (next !== animated) {
|
|
33
|
+
animated = next;
|
|
34
|
+
skeletons.forEach(s => s.update({ IsAnimated: next }));
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
destroy() { skeletons.forEach(s => s.destroy()); },
|
|
38
|
+
};
|
|
39
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const mapUrls = {World: new URL('./maps/world.geojson', import.meta.url).href, US: new URL('./maps/us-states.geojson', import.meta.url).href};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { registerMap } from 'echarts/core';
|
|
2
|
+
import type {GeoJSON, GeoMapOptions} from './catalogue-types.ts';
|
|
3
|
+
import {validateGeoJSON,validateMapRegions} from './catalogue-model.ts';
|
|
4
|
+
import {limits} from './limits.ts';
|
|
5
|
+
import {mapUrls} from './map-assets.ts';
|
|
6
|
+
import {boundedAwait} from './async.ts';
|
|
7
|
+
|
|
8
|
+
export async function fetchMap(url:string,signal?:AbortSignal):Promise<GeoJSON> {
|
|
9
|
+
const controller=new AbortController(), abort=()=>controller.abort(signal?.reason);
|
|
10
|
+
signal?.addEventListener('abort',abort,{once:true}); if(signal?.aborted)abort();
|
|
11
|
+
try {
|
|
12
|
+
return await boundedAwait((async()=>{
|
|
13
|
+
const parsed=new URL(url,typeof location==='undefined'?'http://localhost/':location.href);
|
|
14
|
+
if(!['http:','https:'].includes(parsed.protocol)||parsed.username||parsed.password)throw new Error('Map URL must be an HTTP resource without embedded credentials.');
|
|
15
|
+
const response=await fetch(parsed.href,{signal:controller.signal,credentials:'same-origin'});
|
|
16
|
+
if(!response.ok)throw new Error(`Map resource failed (${response.status}).`);
|
|
17
|
+
if(Number(response.headers.get('Content-Length'))>limits.mapBytes)throw new Error('Map resource exceeds the size limit.');
|
|
18
|
+
let text='';
|
|
19
|
+
if(response.body) { const reader=response.body.getReader(),decoder=new TextDecoder();let bytes=0;
|
|
20
|
+
try { while(true) { const part=await reader.read();if(part.done)break;bytes+=part.value.length;if(bytes>limits.mapBytes)throw new Error('Map resource exceeds the size limit.');text+=decoder.decode(part.value,{stream:true}); }text+=decoder.decode(); }
|
|
21
|
+
finally {await reader.cancel().catch(()=>{});}
|
|
22
|
+
} else {text=await response.text();if(new TextEncoder().encode(text).length>limits.mapBytes)throw new Error('Map resource exceeds the size limit.');}
|
|
23
|
+
controller.signal.throwIfAborted(); const geo=JSON.parse(text);validateGeoJSON(geo);return geo;
|
|
24
|
+
})(),controller.signal,10000,'Map resource timed out. Retry the update.');
|
|
25
|
+
} finally {controller.abort();signal?.removeEventListener('abort',abort);}
|
|
26
|
+
}
|
|
27
|
+
/** Finite reusable slots prevent ECharts' global registry growing on each navigation. */
|
|
28
|
+
export function createMapRegistry(register:(name:string,geo:any)=>void=registerMap,capacity=16) {
|
|
29
|
+
const slots:{name:string;signature:string;refs:number}[]=[];
|
|
30
|
+
return {
|
|
31
|
+
acquire(geo:GeoJSON) {
|
|
32
|
+
const signature=JSON.stringify(geo);
|
|
33
|
+
if(new TextEncoder().encode(signature).length>limits.mapBytes)throw new Error('Map resource exceeds the size limit.');
|
|
34
|
+
let slot=slots.find(s=>s.refs>0&&s.signature===signature);
|
|
35
|
+
if(!slot) {slot=slots.find(s=>!s.refs);if(!slot) {if(slots.length>=capacity)throw new Error('Too many different maps are active.');slot={name:`blu-charts-map-v1-${slots.length}`,signature:'',refs:0};slots.push(slot);}register(slot.name,geo);slot.signature=signature;}
|
|
36
|
+
slot.refs++;let released=false;const owned=slot;
|
|
37
|
+
return {name:slot.name,release(){if(released)return;released=true;if(!--owned.refs){owned.signature='';register(owned.name,{type:'FeatureCollection',features:[]});}}};
|
|
38
|
+
},
|
|
39
|
+
stats(){return {slots:slots.length,active:slots.filter(s=>s.refs>0).length};}
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
export const mapRegistry=createMapRegistry();
|
|
43
|
+
export async function resolveMap(o:GeoMapOptions,signal:AbortSignal) {
|
|
44
|
+
const geo=o.Map==='Custom'?(o.GeoJSON??await fetchMap(o.MapURL!,signal)):await fetchMap(o.Map==='US'?mapUrls.US:mapUrls.World,signal);
|
|
45
|
+
signal.throwIfAborted();validateGeoJSON(geo);validateMapRegions(o,geo);return geo;
|
|
46
|
+
}
|
package/src/messages.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type {ChartOptions} from './types.ts';
|
|
2
|
+
/** The native Block maps its translatable text structure to these plain-text keys. */
|
|
3
|
+
export function chartMessage(o:ChartOptions,key:string,fallback:string,values:Record<string,string>={}) {
|
|
4
|
+
return (o.Messages?.[key]??fallback).replace(/\{(\w+)\}/g,(token,name)=>values[name]??token);
|
|
5
|
+
}
|
package/src/model.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { ChartOptions, MixedChartOptions, HeatmapOptions, MixedPoint, HeatmapPoint } from './types.ts';
|
|
2
|
+
|
|
3
|
+
export const defaults = { Title: 'Chart', Description: '', Height: 320, ValueFormat: 'Number', CurrencyCode: 'USD', Locale: 'en-US', UseCustomPrecision: false, DecimalPlaces: 2, AccessibleLabel: '', ExtendedClass: '', ResetKey: '', Messages: {}, ShowHeader: true, ShowToolbar: true, ShowLegend: true, LegendStyle: 'Standard', LegendPosition: 'Top', LegendShowValues: true, LegendShowPercentages: false, ShowTooltip: true, ShowLabels: false, ShowDataTable: true, ShowPatterns: false, EnableSelection: true, EnableAnimation: true, IsLoading: false, EmptyMessage: 'No data to display', SelectedKey: null, ExportFootnote: 'BluKavantUI Charts' } satisfies Required<ChartOptions>;
|
|
4
|
+
export function formatValue(value: number | null, options: ChartOptions, compact = false) {
|
|
5
|
+
if (value === null) return options.Messages?.NoData ?? 'No data';
|
|
6
|
+
return new Intl.NumberFormat(options.Locale || 'en-US', {
|
|
7
|
+
style: options.ValueFormat === 'Currency' ? 'currency' : options.ValueFormat === 'Percent' ? 'percent' : 'decimal',
|
|
8
|
+
currency: options.CurrencyCode || 'USD', notation: compact ? 'compact' : 'standard',
|
|
9
|
+
...(compact ? {maximumFractionDigits: 1} : options.UseCustomPrecision ? {minimumFractionDigits: options.DecimalPlaces ?? 2, maximumFractionDigits: options.DecimalPlaces ?? 2} : options.ValueFormat === 'Currency' ? {} : {maximumFractionDigits: 12}),
|
|
10
|
+
}).format(value);
|
|
11
|
+
}
|
|
12
|
+
/** Raw numeric data, independent of chart display precision or currency formatting. */
|
|
13
|
+
export function exactValue(value: number | null, original?: string) { return value === null ? 'No data' : original ?? String(value); }
|
|
14
|
+
export function mixedPoint(options: MixedChartOptions, categoryIndex: number, seriesIndex = 0): MixedPoint | null {
|
|
15
|
+
const category = options.Categories[categoryIndex], series = options.Series[seriesIndex];
|
|
16
|
+
return category && series ? { Key: category.Key, CategoryLabel: category.Label, SeriesKey: series.Key, SeriesName: series.Name, Value: series.Values[categoryIndex] } : null;
|
|
17
|
+
}
|
|
18
|
+
export function heatmapPoint(options: HeatmapOptions, index: number): HeatmapPoint | null {
|
|
19
|
+
const cell = options.Cells[index];
|
|
20
|
+
if (!cell) return null;
|
|
21
|
+
return { ...cell, XLabel: options.XCategories.find(c => c.Key === cell.XKey)!.Label, YLabel: options.YCategories.find(c => c.Key === cell.YKey)!.Label };
|
|
22
|
+
}
|
|
23
|
+
export function heatmapRange(options: HeatmapOptions) {
|
|
24
|
+
let min = 0, max = 0;
|
|
25
|
+
for (const cell of options.Cells) if (cell.Value !== null) { min = Math.min(min, cell.Value); max = Math.max(max, cell.Value); }
|
|
26
|
+
min = options.MinValue ?? min; max = options.MaxValue ?? max;
|
|
27
|
+
if (min >= max) {
|
|
28
|
+
if (options.MaxValue !== undefined) min = max - 1;
|
|
29
|
+
else max = min + 1;
|
|
30
|
+
}
|
|
31
|
+
return { min, max };
|
|
32
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** Optional motion factory; preserves the original chart contract. */
|
|
2
|
+
import type {ComponentMotionSettings} from '@blukavantui/community/motion/settings';
|
|
3
|
+
import {configuredChart,type MotionState} from '../motion-policy.ts';
|
|
4
|
+
import {use} from 'echarts/core';
|
|
5
|
+
import {createChartRuntime} from '../catalogue-runtime.ts';
|
|
6
|
+
import {BarChart} from 'echarts/charts';
|
|
7
|
+
import {GridComponent} from 'echarts/components';
|
|
8
|
+
import {buildOption as bar} from '../builders/bar.ts';
|
|
9
|
+
import type {CartesianOptions,ChartPoint} from '../catalogue-types.ts';
|
|
10
|
+
import type {ChartEvents} from '../types.ts';
|
|
11
|
+
export function createMotionBarChart(host: HTMLElement, options: CartesianOptions, motion: MotionState, events: ChartEvents<ChartPoint> = {}, settings:ComponentMotionSettings = {}) {
|
|
12
|
+
return configuredChart(host,motion,settings,(policy,host)=>{
|
|
13
|
+
use([BarChart, GridComponent]); return createChartRuntime(host, 'bar', options, events, bar, policy);
|
|
14
|
+
});
|
|
15
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** Optional motion factory; preserves the original chart contract. */
|
|
2
|
+
import type {ComponentMotionSettings} from '@blukavantui/community/motion/settings';
|
|
3
|
+
import {configuredChart,type MotionState} from '../motion-policy.ts';
|
|
4
|
+
import {use} from 'echarts/core';
|
|
5
|
+
import {createChartRuntime} from '../catalogue-runtime.ts';
|
|
6
|
+
import {PieChart} from 'echarts/charts';
|
|
7
|
+
import {buildOption as doughnut} from '../builders/doughnut.ts';
|
|
8
|
+
import type {ChartPoint,CircularOptions} from '../catalogue-types.ts';
|
|
9
|
+
import type {ChartEvents} from '../types.ts';
|
|
10
|
+
export function createMotionDoughnutChart(host: HTMLElement, options: CircularOptions, motion: MotionState, events: ChartEvents<ChartPoint> = {}, settings:ComponentMotionSettings = {}) {
|
|
11
|
+
return configuredChart(host,motion,settings,(policy,host)=>{
|
|
12
|
+
use([PieChart]); return createChartRuntime<CircularOptions, ChartPoint>(host, 'doughnut', options, events, doughnut, policy);
|
|
13
|
+
});
|
|
14
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** Optional motion factory; preserves the original chart contract. */
|
|
2
|
+
import type {ComponentMotionSettings} from '@blukavantui/community/motion/settings';
|
|
3
|
+
import {configuredChart,type MotionState} from '../motion-policy.ts';
|
|
4
|
+
import {use} from 'echarts/core';
|
|
5
|
+
import {createChartRuntime} from '../catalogue-runtime.ts';
|
|
6
|
+
import {HeatmapChart} from 'echarts/charts';
|
|
7
|
+
import {GridComponent,VisualMapComponent} from 'echarts/components';
|
|
8
|
+
import {heatmapOption as heatmap} from '../builders/heatmap.ts';
|
|
9
|
+
import type {ChartEvents,HeatmapOptions,HeatmapPoint} from '../types.ts';
|
|
10
|
+
export function createMotionHeatmap(host: HTMLElement, options: HeatmapOptions, motion: MotionState, events: ChartEvents<HeatmapPoint> = {}, settings:ComponentMotionSettings = {}) {
|
|
11
|
+
return configuredChart(host,motion,settings,(policy,host)=>{
|
|
12
|
+
use([HeatmapChart, GridComponent, VisualMapComponent]); return createChartRuntime<HeatmapOptions, HeatmapPoint>(host, 'heatmap', options, events, heatmap, policy);
|
|
13
|
+
});
|
|
14
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** Optional motion factory; preserves the original chart contract. */
|
|
2
|
+
import type {ComponentMotionSettings} from '@blukavantui/community/motion/settings';
|
|
3
|
+
import {configuredChart,type MotionState} from '../motion-policy.ts';
|
|
4
|
+
import {use} from 'echarts/core';
|
|
5
|
+
import {createChartRuntime} from '../catalogue-runtime.ts';
|
|
6
|
+
import {LineChart} from 'echarts/charts';
|
|
7
|
+
import {GridComponent} from 'echarts/components';
|
|
8
|
+
import {buildOption as line} from '../builders/line.ts';
|
|
9
|
+
import type {CartesianOptions,ChartPoint} from '../catalogue-types.ts';
|
|
10
|
+
import type {ChartEvents} from '../types.ts';
|
|
11
|
+
export function createMotionLineChart(host: HTMLElement, options: CartesianOptions, motion: MotionState, events: ChartEvents<ChartPoint> = {}, settings:ComponentMotionSettings = {}) {
|
|
12
|
+
return configuredChart(host,motion,settings,(policy,host)=>{
|
|
13
|
+
use([LineChart, GridComponent]); return createChartRuntime(host, 'line', options, events, line, policy);
|
|
14
|
+
});
|
|
15
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** Optional motion factory; preserves the original chart contract. */
|
|
2
|
+
import type {ComponentMotionSettings} from '@blukavantui/community/motion/settings';
|
|
3
|
+
import {configuredChart,type MotionState} from '../motion-policy.ts';
|
|
4
|
+
import {use} from 'echarts/core';
|
|
5
|
+
import {createChartRuntime} from '../catalogue-runtime.ts';
|
|
6
|
+
import {PieChart} from 'echarts/charts';
|
|
7
|
+
import {buildOption as pie} from '../builders/pie.ts';
|
|
8
|
+
import type {ChartPoint,CircularOptions} from '../catalogue-types.ts';
|
|
9
|
+
import type {ChartEvents} from '../types.ts';
|
|
10
|
+
export function createMotionPieChart(host: HTMLElement, options: CircularOptions, motion: MotionState, events: ChartEvents<ChartPoint> = {}, settings:ComponentMotionSettings = {}) {
|
|
11
|
+
return configuredChart(host,motion,settings,(policy,host)=>{
|
|
12
|
+
use([PieChart]); return createChartRuntime<CircularOptions, ChartPoint>(host, 'pie', options, events, pie, policy);
|
|
13
|
+
});
|
|
14
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** Optional motion factory; preserves the original chart contract. */
|
|
2
|
+
import type {ComponentMotionSettings} from '@blukavantui/community/motion/settings';
|
|
3
|
+
import {configuredChart,type MotionState} from '../motion-policy.ts';
|
|
4
|
+
import {use} from 'echarts/core';
|
|
5
|
+
import {createChartRuntime} from '../catalogue-runtime.ts';
|
|
6
|
+
import {RadarChart} from 'echarts/charts';
|
|
7
|
+
import {RadarComponent} from 'echarts/components';
|
|
8
|
+
import {buildOption as radar} from '../builders/radar.ts';
|
|
9
|
+
import type {ChartPoint,RadarOptions} from '../catalogue-types.ts';
|
|
10
|
+
import type {ChartEvents} from '../types.ts';
|
|
11
|
+
export function createMotionRadarChart(host: HTMLElement, options: RadarOptions, motion: MotionState, events: ChartEvents<ChartPoint> = {}, settings:ComponentMotionSettings = {}) {
|
|
12
|
+
return configuredChart(host,motion,settings,(policy,host)=>{
|
|
13
|
+
use([RadarChart, RadarComponent]); return createChartRuntime<RadarOptions, ChartPoint>(host, 'radar', options, events, radar, policy);
|
|
14
|
+
});
|
|
15
|
+
}
|