From 6465207250913aaf5d80be908598bef84b6e9120 Mon Sep 17 00:00:00 2001 From: Copilot Date: Fri, 12 Jun 2026 15:51:48 +0000 Subject: [PATCH 1/2] feat(templates): add Doughnut & Combo (Chart.js), Network Graph & Tree (ECharts) New common chart templates, each flagged with * in gallery labels so they are easy to spot for inspection: - Chart.js Doughnut: pie variant with a hollow centre (cutout) - Chart.js Combo (Bar + Line): dual-axis bars + line, degrades to bars - ECharts Network Graph: node-link from an edge list; node size = degree; deterministic circular layout (force available as a property) - ECharts Tree: orthogonal dendrogram from a category hierarchy All four were rendered to PNG and visually graded across dev/gallery datasets and a larger stress pass; circular-graph label margins were widened so labels never clip. typecheck/lint/tests/site build all pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../flint-js/src/chartjs/templates/combo.ts | 146 +++++++++++++ .../src/chartjs/templates/doughnut.ts | 102 +++++++++ .../flint-js/src/chartjs/templates/index.ts | 6 +- .../flint-js/src/echarts/templates/graph.ts | 167 ++++++++++++++ .../flint-js/src/echarts/templates/index.ts | 6 +- .../flint-js/src/echarts/templates/tree.ts | 145 +++++++++++++ .../flint-js/src/test-data/chartjs-tests.ts | 152 ++++++++++++- .../flint-js/src/test-data/echarts-tests.ts | 168 ++++++++++++++ packages/flint-js/src/test-data/index.ts | 12 +- .../test_cases/combo_climograph.json | 93 ++++++++ .../test_cases/combo_revenue_growth.json | 93 ++++++++ .../test_cases/combo_single_measure.json | 57 +++++ .../test_cases/doughnut_browser_share.json | 49 +++++ .../test_cases/doughnut_counts.json | 205 ++++++++++++++++++ .../test_cases/doughnut_many_slices.json | 65 ++++++ .../chartjs-testing/test_cases/manifest.json | 24 ++ .../test_cases/graph_dense_16.json | 188 ++++++++++++++++ .../test_cases/graph_hub_spoke.json | 83 +++++++ .../test_cases/graph_team.json | 73 +++++++ .../echarts-testing/test_cases/manifest.json | 34 ++- .../test_cases/tree_many_leaves.json | 148 +++++++++++++ .../echarts-testing/test_cases/tree_org.json | 88 ++++++++ .../test_cases/tree_regions.json | 49 +++++ site/src/shared/chart-categories.ts | 4 + 24 files changed, 2143 insertions(+), 14 deletions(-) create mode 100644 packages/flint-js/src/chartjs/templates/combo.ts create mode 100644 packages/flint-js/src/chartjs/templates/doughnut.ts create mode 100644 packages/flint-js/src/echarts/templates/graph.ts create mode 100644 packages/flint-js/src/echarts/templates/tree.ts create mode 100644 recursive/chartjs-testing/test_cases/combo_climograph.json create mode 100644 recursive/chartjs-testing/test_cases/combo_revenue_growth.json create mode 100644 recursive/chartjs-testing/test_cases/combo_single_measure.json create mode 100644 recursive/chartjs-testing/test_cases/doughnut_browser_share.json create mode 100644 recursive/chartjs-testing/test_cases/doughnut_counts.json create mode 100644 recursive/chartjs-testing/test_cases/doughnut_many_slices.json create mode 100644 recursive/echarts-testing/test_cases/graph_dense_16.json create mode 100644 recursive/echarts-testing/test_cases/graph_hub_spoke.json create mode 100644 recursive/echarts-testing/test_cases/graph_team.json create mode 100644 recursive/echarts-testing/test_cases/tree_many_leaves.json create mode 100644 recursive/echarts-testing/test_cases/tree_org.json create mode 100644 recursive/echarts-testing/test_cases/tree_regions.json diff --git a/packages/flint-js/src/chartjs/templates/combo.ts b/packages/flint-js/src/chartjs/templates/combo.ts new file mode 100644 index 00000000..ee89719b --- /dev/null +++ b/packages/flint-js/src/chartjs/templates/combo.ts @@ -0,0 +1,146 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** + * Chart.js Combo (Bar + Line) template. + * + * A dual-measure chart: bars for a primary measure and a line for a secondary + * measure plotted on its own right-hand axis. This is one of the most common + * dashboard idioms (e.g. revenue bars + growth-rate line). + * + * Data model: + * x (nominal) → shared category axis + * y (quantitative) → the BAR measure (left axis) + * line measure → the LINE measure (right axis); chosen from + * `chartProperties.lineField`, else the first other + * numeric field in the data. + * color is intentionally not used — the two series ARE the legend. + * + * Chart.js renders this as a base `type: 'bar'` chart with one dataset + * overridden to `type: 'line'` and bound to a secondary `y1` scale. + */ + +import { ChartTemplateDef } from '../../core/types'; +import { + extractCategories, + buildCategoryAlignedData, + getChartJsPalette, + getSeriesBorderColor, + getSeriesBackgroundColor, +} from './utils'; +import { detectBandedAxisFromSemantics } from '../../vegalite/templates/utils'; + +/** A field is numeric if (nearly) all of its non-null values parse as numbers. */ +function isNumericField(table: any[], field: string): boolean { + let total = 0; + let numeric = 0; + for (const row of table) { + const v = row[field]; + if (v == null || v === '') continue; + total++; + if (typeof v === 'number' ? isFinite(v) : !isNaN(Number(v))) numeric++; + } + return total > 0 && numeric / total >= 0.9; +} + +export const cjsComboChartDef: ChartTemplateDef = { + chart: 'Combo Chart', + template: { mark: 'bar', encoding: {} }, + channels: ['x', 'y', 'column', 'row'], + markCognitiveChannel: 'length', + declareLayoutMode: (cs, table) => { + const result = detectBandedAxisFromSemantics(cs, table, { preferAxis: 'x' }); + return { + axisFlags: result ? { [result.axis]: { banded: true } } : { x: { banded: true } }, + resolvedTypes: result?.resolvedTypes, + }; + }, + instantiate: (spec, ctx) => { + const { channelSemantics, table, chartProperties } = ctx; + const catField = channelSemantics.x?.field; + const barField = channelSemantics.y?.field; + if (!catField || !barField || table.length === 0) return; + + // Resolve the line measure: explicit override, else first other numeric field. + const lineField: string | undefined = + (chartProperties?.lineField && chartProperties.lineField in table[0]) + ? chartProperties.lineField + : Object.keys(table[0]).find( + (k) => k !== catField && k !== barField && isNumericField(table, k), + ); + + const categories = extractCategories(table, catField, channelSemantics.x?.ordinalSortOrder); + const barData = buildCategoryAlignedData(table, catField, barField, categories); + + const palette = getChartJsPalette(ctx, 'color'); + + const datasets: any[] = [{ + type: 'bar' as const, + label: barField, + data: barData, + yAxisID: 'y', + order: 2, + backgroundColor: getSeriesBackgroundColor(palette, 0), + borderColor: getSeriesBorderColor(palette, 0), + borderWidth: 1, + borderRadius: chartProperties?.cornerRadius ?? 0, + }]; + + if (lineField) { + const lineData = buildCategoryAlignedData(table, catField, lineField, categories); + datasets.push({ + type: 'line' as const, + label: lineField, + data: lineData, + yAxisID: 'y1', + order: 1, + borderColor: getSeriesBorderColor(palette, 1), + backgroundColor: 'transparent', + borderWidth: 2, + pointRadius: 3, + tension: 0.3, + fill: false, + }); + } + + const config: any = { + type: 'bar', + data: { labels: categories, datasets }, + options: { + responsive: true, + maintainAspectRatio: false, + scales: { + x: { + title: { display: true, text: catField }, + }, + y: { + type: 'linear' as const, + position: 'left' as const, + beginAtZero: channelSemantics.y?.zero ? channelSemantics.y.zero.zero !== false : true, + title: { display: true, text: barField }, + }, + ...(lineField ? { + y1: { + type: 'linear' as const, + position: 'right' as const, + title: { display: true, text: lineField }, + // Don't draw the right axis grid over the bars. + grid: { drawOnChartArea: false }, + }, + } : {}), + }, + plugins: { + legend: { display: true }, + tooltip: { enabled: true }, + }, + }, + }; + + Object.assign(spec, config); + delete spec.mark; + delete spec.encoding; + }, + properties: [ + { key: 'cornerRadius', label: 'Corners', type: 'continuous', min: 0, max: 15, step: 1, defaultValue: 0 }, + ], +}; diff --git a/packages/flint-js/src/chartjs/templates/doughnut.ts b/packages/flint-js/src/chartjs/templates/doughnut.ts new file mode 100644 index 00000000..6341a226 --- /dev/null +++ b/packages/flint-js/src/chartjs/templates/doughnut.ts @@ -0,0 +1,102 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** + * Chart.js Doughnut Chart template. + * + * Contrast with Pie: + * Pie: type = 'pie' (full disc, area encodes value) + * Doughnut: type = 'doughnut' with a `cutout` — the hollow centre trades a + * little area-judgement accuracy for a cleaner look and room for a + * centre label. Same data model as Pie (color = slice, size = value). + */ + +import { ChartTemplateDef, ChartPropertyDef } from '../../core/types'; +import { + extractCategories, + getChartJsPalette, + getSeriesBorderColor, + getSeriesBackgroundColor, +} from './utils'; + +export const cjsDoughnutChartDef: ChartTemplateDef = { + chart: 'Doughnut Chart', + template: { mark: 'arc', encoding: {} }, + channels: ['size', 'color', 'column', 'row'], + markCognitiveChannel: 'area', + instantiate: (spec, ctx) => { + const { channelSemantics, table, chartProperties } = ctx; + const colorField = channelSemantics.color?.field; + const sizeField = channelSemantics.size?.field; + + const labels: string[] = []; + const values: number[] = []; + + const palette = getChartJsPalette(ctx, 'color'); + + if (colorField && sizeField) { + const agg = new Map(); + for (const row of table) { + const cat = String(row[colorField] ?? ''); + const val = Number(row[sizeField]) || 0; + agg.set(cat, (agg.get(cat) ?? 0) + val); + } + const categories = extractCategories(table, colorField, channelSemantics.color?.ordinalSortOrder); + for (const cat of categories) { + labels.push(cat); + values.push(agg.get(cat) ?? 0); + } + } else if (colorField) { + const counts = new Map(); + for (const row of table) { + const cat = String(row[colorField] ?? ''); + counts.set(cat, (counts.get(cat) ?? 0) + 1); + } + const categories = extractCategories(table, colorField, channelSemantics.color?.ordinalSortOrder); + for (const cat of categories) { + labels.push(cat); + values.push(counts.get(cat) ?? 0); + } + } else if (sizeField) { + for (const row of table) { + const val = Number(row[sizeField]) || 0; + labels.push(String(val)); + values.push(val); + } + } + + // Default to a 55% hollow centre; let callers tune it. + const cutout = chartProperties?.innerRadius ?? 55; + + const config: any = { + type: 'doughnut', + data: { + labels, + datasets: [{ + data: values, + backgroundColor: labels.map((_, i) => getSeriesBackgroundColor(palette, i, 0.6)), + borderColor: labels.map((_, i) => getSeriesBorderColor(palette, i)), + borderWidth: 1, + }], + }, + options: { + responsive: true, + maintainAspectRatio: false, + cutout: `${cutout}%`, + plugins: { + legend: { display: true, position: 'right' as const }, + tooltip: { enabled: true }, + }, + }, + _width: Math.max(ctx.canvasSize.width, 300), + _height: Math.max(ctx.canvasSize.height, 250), + }; + + Object.assign(spec, config); + delete spec.mark; + delete spec.encoding; + }, + properties: [ + { key: 'innerRadius', label: 'Hole', type: 'continuous', min: 20, max: 80, step: 5, defaultValue: 55 } as ChartPropertyDef, + ], +}; diff --git a/packages/flint-js/src/chartjs/templates/index.ts b/packages/flint-js/src/chartjs/templates/index.ts index b9b3356b..57f4c0e8 100644 --- a/packages/flint-js/src/chartjs/templates/index.ts +++ b/packages/flint-js/src/chartjs/templates/index.ts @@ -12,9 +12,11 @@ import { ChartTemplateDef } from '../../core/types'; import { cjsScatterPlotDef } from './scatter'; import { cjsBubbleChartDef } from './bubble'; import { cjsBarChartDef, cjsStackedBarChartDef, cjsGroupedBarChartDef } from './bar'; +import { cjsComboChartDef } from './combo'; import { cjsLineChartDef } from './line'; import { cjsAreaChartDef } from './area'; import { cjsPieChartDef } from './pie'; +import { cjsDoughnutChartDef } from './doughnut'; import { cjsHistogramDef } from './histogram'; import { cjsRadarChartDef } from './radar'; import { cjsRoseChartDef } from './rose'; @@ -24,9 +26,9 @@ import { cjsRoseChartDef } from './rose'; */ export const cjsTemplateDefs: { [key: string]: ChartTemplateDef[] } = { 'Scatter & Point': [cjsScatterPlotDef, cjsBubbleChartDef], - 'Bar': [cjsBarChartDef, cjsGroupedBarChartDef, cjsStackedBarChartDef, cjsHistogramDef], + 'Bar': [cjsBarChartDef, cjsGroupedBarChartDef, cjsStackedBarChartDef, cjsComboChartDef, cjsHistogramDef], 'Line & Area': [cjsLineChartDef, cjsAreaChartDef], - 'Part-to-Whole': [cjsPieChartDef], + 'Part-to-Whole': [cjsPieChartDef, cjsDoughnutChartDef], 'Polar': [cjsRadarChartDef, cjsRoseChartDef], }; diff --git a/packages/flint-js/src/echarts/templates/graph.ts b/packages/flint-js/src/echarts/templates/graph.ts new file mode 100644 index 00000000..0bc3ce54 --- /dev/null +++ b/packages/flint-js/src/echarts/templates/graph.ts @@ -0,0 +1,167 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** + * ECharts Graph (Network) template. + * + * Unique to ECharts — no Vega-Lite equivalent. + * Renders relationship/edge-list data as a node-link network. Node size + * encodes degree (number of connections), so hubs stand out. + * + * Data model (each row = one edge): + * x (nominal): source node name + * y (nominal): target node name + * size (quantitative, optional): edge weight (defaults to 1) + * + * Nodes are derived from the union of source/target values. Parallel edges + * (same source→target pair) are aggregated (summed weight). + * + * Layout defaults to `circular` so the render is deterministic (no force + * simulation needed to settle); a `force` layout is available as a property. + */ + +import { ChartTemplateDef, ChartPropertyDef } from '../../core/types'; +import { DEFAULT_COLORS } from './utils'; +import { getPaletteForScheme } from '../colormap'; + +export const ecGraphDef: ChartTemplateDef = { + chart: 'Network Graph', + template: { mark: 'point', encoding: {} }, + channels: ['x', 'y', 'size'], + markCognitiveChannel: 'position', + instantiate: (spec, ctx) => { + const { channelSemantics, table, chartProperties, colorDecisions } = ctx; + const sourceField = channelSemantics.x?.field; + const targetField = channelSemantics.y?.field; + const weightField = channelSemantics.size?.field; + if (!sourceField || !targetField) return; + + // Aggregate parallel edges and accumulate per-node degree (weighted). + const linkAgg = new Map(); + const degree = new Map(); + for (const row of table) { + const src = String(row[sourceField] ?? ''); + const tgt = String(row[targetField] ?? ''); + if (!src || !tgt || src === tgt) continue; + const w = weightField ? (Number(row[weightField]) || 0) : 1; + const key = `${src}\x00${tgt}`; + linkAgg.set(key, (linkAgg.get(key) ?? 0) + w); + degree.set(src, (degree.get(src) ?? 0) + w); + degree.set(tgt, (degree.get(tgt) ?? 0) + w); + } + + const links: { source: string; target: string; value: number }[] = []; + const nodeSet = new Set(); + for (const [key, value] of linkAgg) { + const [source, target] = key.split('\x00'); + nodeSet.add(source); + nodeSet.add(target); + links.push({ source, target, value }); + } + if (links.length === 0) return; + + const nodeArr = [...nodeSet]; + + // ── Palette ─────────────────────────────────────────────────────── + const decision = colorDecisions?.color ?? colorDecisions?.group; + let palette: string[] | undefined; + if (decision?.schemeId) { + const fromRegistry = getPaletteForScheme(decision.schemeId); + if (fromRegistry && fromRegistry.length > 0) palette = fromRegistry; + } + if (!palette || palette.length === 0) { + palette = getPaletteForScheme(nodeArr.length > 10 ? 'cat20' : 'cat10') ?? DEFAULT_COLORS; + } + + // ── Node sizing: area-proportional to degree ───────────────────────── + const degVals = nodeArr.map((n) => degree.get(n) ?? 0); + const dMin = Math.min(...degVals); + const dMax = Math.max(...degVals); + const rMin = 12, rMax = 46; + const sizeFor = (d: number) => { + if (dMax === dMin) return (rMin + rMax) / 2; + const t = (d - dMin) / (dMax - dMin); + // Interpolate area, then convert back to a diameter. + const area = rMin * rMin + t * (rMax * rMax - rMin * rMin); + return Math.sqrt(area); + }; + + const nodes = nodeArr.map((name, i) => ({ + name, + value: degree.get(name) ?? 0, + symbolSize: sizeFor(degree.get(name) ?? 0), + itemStyle: { color: palette![i % palette!.length] }, + })); + + // ── Edge widths scaled to weight ───────────────────────────────────── + const wVals = links.map((l) => l.value); + const wMin = Math.min(...wVals); + const wMax = Math.max(...wVals); + const widthFor = (v: number) => { + if (wMax === wMin) return 1.4; + return 0.8 + ((v - wMin) / (wMax - wMin)) * 3.2; + }; + + const layout = chartProperties?.layout === 'force' ? 'force' : 'circular'; + + // Square canvas; grow with node count. Extra padding leaves room for the + // radial node labels so they don't clip at the canvas edge. + const side = Math.max(420, Math.min(860, Math.round(Math.sqrt(nodeArr.length) * 155) + 40)); + const pad = 64; + + const option: any = { + tooltip: { + trigger: 'item', + formatter: (params: any) => { + if (params.dataType === 'edge') { + return `${params.data.source} → ${params.data.target}
Weight: ${params.data.value}`; + } + return `${params.name}
Degree: ${params.value}`; + }, + }, + series: [{ + type: 'graph', + layout, + data: nodes, + links: links.map((l) => ({ ...l, lineStyle: { width: widthFor(l.value) } })), + roam: false, + label: { + show: true, + position: 'right', + fontSize: 11, + color: '#333', + }, + lineStyle: { + color: 'source', + opacity: 0.5, + curveness: layout === 'circular' ? 0.3 : 0, + }, + emphasis: { + focus: 'adjacency', + lineStyle: { width: 4 }, + }, + circular: { rotateLabel: true }, + force: { repulsion: 180, edgeLength: [50, 130], gravity: 0.08 }, + left: pad, + right: pad, + top: pad, + bottom: pad, + }], + color: palette ?? DEFAULT_COLORS, + _width: side, + _height: side, + }; + + Object.assign(spec, option); + delete spec.mark; + delete spec.encoding; + }, + properties: [ + { + key: 'layout', label: 'Layout', type: 'discrete', options: [ + { value: 'circular', label: 'Circular (default)' }, + { value: 'force', label: 'Force-directed' }, + ], + } as ChartPropertyDef, + ], +}; diff --git a/packages/flint-js/src/echarts/templates/index.ts b/packages/flint-js/src/echarts/templates/index.ts index c80ddafb..ca4eeb9b 100644 --- a/packages/flint-js/src/echarts/templates/index.ts +++ b/packages/flint-js/src/echarts/templates/index.ts @@ -34,6 +34,8 @@ import { ecRangedDotPlotDef } from './ranged-dot'; import { ecDensityPlotDef } from './density'; import { ecCalendarHeatmapDef } from './calendar'; import { ecParallelCoordinatesDef } from './parallel'; +import { ecGraphDef } from './graph'; +import { ecTreeDef } from './tree'; /** * ECharts chart template definitions, grouped by category. @@ -43,13 +45,13 @@ export const ecTemplateDefs: { [key: string]: ChartTemplateDef[] } = { 'Scatter & Point': [ecScatterPlotDef, ecRegressionDef, ecRangedDotPlotDef, ecBoxplotDef, ecStripPlotDef], 'Bar': [ecBarChartDef, ecGroupedBarChartDef, ecStackedBarChartDef, ecLollipopChartDef, ecPyramidChartDef, ecHeatmapDef, ecCalendarHeatmapDef], 'Line & Area': [ecLineChartDef, ecBumpChartDef, ecAreaChartDef, ecStreamgraphDef], - 'Part-to-Whole': [ecPieChartDef, ecFunnelChartDef, ecTreemapDef, ecSunburstDef], + 'Part-to-Whole': [ecPieChartDef, ecFunnelChartDef, ecTreemapDef, ecSunburstDef, ecTreeDef], 'Statistical': [ecHistogramDef, ecDensityPlotDef, ecParallelCoordinatesDef], 'Financial': [ecCandlestickDef], 'Other': [ecWaterfallChartDef], 'Polar': [ecRadarChartDef, ecRoseChartDef], 'Indicator': [ecGaugeChartDef], - 'Flow': [ecSankeyDef], + 'Flow': [ecSankeyDef, ecGraphDef], }; /** diff --git a/packages/flint-js/src/echarts/templates/tree.ts b/packages/flint-js/src/echarts/templates/tree.ts new file mode 100644 index 00000000..73091bd2 --- /dev/null +++ b/packages/flint-js/src/echarts/templates/tree.ts @@ -0,0 +1,145 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** + * ECharts Tree template. + * + * Unique to ECharts — no Vega-Lite equivalent. + * Renders a hierarchy as an orthogonal node-link tree (dendrogram). Good for + * showing parent → child structure where containment/area (treemap) or radial + * nesting (sunburst) would obscure the branching itself. + * + * Data model: + * color (nominal): first-level category (root's children) + * detail (nominal, optional): second-level category (leaves) + * size (quantitative, optional): leaf value (defaults to a count) + * + * color only → two levels: root → categories. + * color + detail → three levels: root → categories → sub-categories. + */ + +import { ChartTemplateDef, ChartPropertyDef } from '../../core/types'; +import { extractCategories, DEFAULT_COLORS } from './utils'; +import { getPaletteForScheme } from '../colormap'; + +export const ecTreeDef: ChartTemplateDef = { + chart: 'Tree', + template: { mark: 'point', encoding: {} }, + channels: ['color', 'detail', 'size'], + markCognitiveChannel: 'position', + instantiate: (spec, ctx) => { + const { channelSemantics, table, chartProperties, colorDecisions } = ctx; + const catField = channelSemantics.color?.field; + const subCatField = channelSemantics.detail?.field; + const valField = channelSemantics.size?.field; + if (!catField) return; + + const categories = extractCategories(table, catField, channelSemantics.color?.ordinalSortOrder); + if (categories.length === 0) return; + + // ── Palette ─────────────────────────────────────────────────────── + const decision = colorDecisions?.color ?? colorDecisions?.group; + let palette: string[] | undefined; + if (decision?.schemeId) { + const fromRegistry = getPaletteForScheme(decision.schemeId); + if (fromRegistry && fromRegistry.length > 0) palette = fromRegistry; + } + if (!palette || palette.length === 0) { + palette = getPaletteForScheme(categories.length > 10 ? 'cat20' : 'cat10') ?? DEFAULT_COLORS; + } + + // ── Build the hierarchy ─────────────────────────────────────────── + let leafCount = 0; + const children = categories.map((cat, catIdx) => { + const catRows = table.filter((r) => String(r[catField]) === cat); + const color = palette![catIdx % palette!.length]; + if (subCatField) { + const subCats = extractCategories(catRows, subCatField); + const subChildren = subCats.map((sub) => { + const subRows = catRows.filter((r) => String(r[subCatField]) === sub); + const value = valField + ? subRows.reduce((s, r) => s + (Number(r[valField]) || 0), 0) + : subRows.length; + leafCount++; + return { name: sub, value, lineStyle: { color }, itemStyle: { color } }; + }); + return { name: cat, children: subChildren, lineStyle: { color }, itemStyle: { color } }; + } + const value = valField + ? catRows.reduce((s, r) => s + (Number(r[valField]) || 0), 0) + : catRows.length; + leafCount++; + return { name: cat, value, lineStyle: { color }, itemStyle: { color } }; + }); + + const rootName = chartProperties?.rootLabel ?? 'All'; + const treeData = [{ name: rootName, children }]; + + const depth = subCatField ? 3 : 2; + const orient = chartProperties?.orient === 'TB' ? 'TB' : 'LR'; + + // ── Layout: depth drives width (LR), leaf count drives height ───────── + const canvasW = Math.max(ctx.canvasSize.width, 340 + (depth - 1) * 210); + const canvasH = Math.max(ctx.canvasSize.height, Math.min(1400, Math.max(300, leafCount * 26))); + + const option: any = { + tooltip: { + trigger: 'item', + triggerOn: 'mousemove', + formatter: (params: any) => { + const v = params.value; + return v != null && v !== '' + ? `${params.name}
Value: ${v}` + : params.name; + }, + }, + series: [{ + type: 'tree', + data: treeData, + layout: 'orthogonal', + orient, + top: 24, + bottom: 24, + left: orient === 'LR' ? 40 : 24, + right: orient === 'LR' ? 140 : 24, + symbol: 'circle', + symbolSize: 8, + initialTreeDepth: -1, + expandAndCollapse: false, + roam: false, + lineStyle: { width: 1.2, curveness: 0.5, color: '#bbb' }, + label: { + show: true, + position: orient === 'LR' ? 'left' : 'top', + verticalAlign: 'middle', + align: orient === 'LR' ? 'right' : 'center', + fontSize: 11, + color: '#333', + }, + leaves: { + label: { + position: orient === 'LR' ? 'right' : 'bottom', + verticalAlign: 'middle', + align: orient === 'LR' ? 'left' : 'center', + }, + }, + emphasis: { focus: 'descendant' }, + }], + color: palette ?? DEFAULT_COLORS, + _width: canvasW, + _height: canvasH, + }; + + Object.assign(spec, option); + delete spec.mark; + delete spec.encoding; + }, + properties: [ + { + key: 'orient', label: 'Orient', type: 'discrete', options: [ + { value: 'LR', label: 'Left → Right (default)' }, + { value: 'TB', label: 'Top → Bottom' }, + ], + } as ChartPropertyDef, + ], +}; diff --git a/packages/flint-js/src/test-data/chartjs-tests.ts b/packages/flint-js/src/test-data/chartjs-tests.ts index 711690f4..8910ba72 100644 --- a/packages/flint-js/src/test-data/chartjs-tests.ts +++ b/packages/flint-js/src/test-data/chartjs-tests.ts @@ -615,9 +615,159 @@ export function genChartJsRoseTests(): TestCase[] { } // --------------------------------------------------------------------------- -// Bubble Chart (NEW — flagged with * for inspection) +// Doughnut Chart (NEW — flagged with * for inspection) // --------------------------------------------------------------------------- +export function genChartJsDoughnutTests(): TestCase[] { + const tests: TestCase[] = []; + const rand = seededRandom(41); + + // 1. Market share (few slices) + { + const browsers = ['Chrome', 'Safari', 'Edge', 'Firefox', 'Other']; + const data = browsers.map((b) => ({ Browser: b, Share: Math.round((5 + rand() * 45) * 10) / 10 })); + tests.push({ + title: 'CJS: Doughnut — Browser Share *', + description: '5-slice doughnut with a 55% hollow centre; color = category, size = value.', + tags: ['chartjs', 'doughnut', 'part-to-whole'], + chartType: 'Doughnut Chart', + data, + fields: [makeField('Browser'), makeField('Share')], + metadata: { + Browser: { type: Type.String, semanticType: 'Category', levels: browsers }, + Share: { type: Type.Number, semanticType: 'Quantity', levels: [] }, + }, + encodingMap: { color: makeEncodingItem('Browser'), size: makeEncodingItem('Share') }, + }); + } + + // 2. Many slices + { + const cats = genCategories('Dept', 9); + const data = cats.map((c) => ({ Dept: c, Budget: Math.round(10 + rand() * 90) })); + tests.push({ + title: 'CJS: Doughnut — Budget by Department (9 slices) *', + description: 'Doughnut with 9 categories; legend on the right.', + tags: ['chartjs', 'doughnut', 'part-to-whole', 'many-categories'], + chartType: 'Doughnut Chart', + data, + fields: [makeField('Dept'), makeField('Budget')], + metadata: { + Dept: { type: Type.String, semanticType: 'Category', levels: cats }, + Budget: { type: Type.Number, semanticType: 'Quantity', levels: [] }, + }, + encodingMap: { color: makeEncodingItem('Dept'), size: makeEncodingItem('Budget') }, + }); + } + + // 3. Count-only (no size field) + { + const types = ['Bug', 'Feature', 'Chore', 'Docs']; + const data = Array.from({ length: 60 }, () => ({ Type: types[Math.floor(rand() * types.length)] })); + tests.push({ + title: 'CJS: Doughnut — Issue Types (counts) *', + description: 'No size channel; slice value is the count of rows per category.', + tags: ['chartjs', 'doughnut', 'part-to-whole', 'count'], + chartType: 'Doughnut Chart', + data, + fields: [makeField('Type')], + metadata: { + Type: { type: Type.String, semanticType: 'Category', levels: types }, + }, + encodingMap: { color: makeEncodingItem('Type') }, + }); + } + + return tests; +} + +// --------------------------------------------------------------------------- +// Combo Chart — Bar + Line (NEW — flagged with * for inspection) +// --------------------------------------------------------------------------- + +export function genChartJsComboTests(): TestCase[] { + const tests: TestCase[] = []; + + // 1. Monthly revenue (bars) + growth-rate % (line, right axis) + { + const rand = seededRandom(53); + const months = genMonths(12); + let prev = 100 + rand() * 40; + const data = months.map((m) => { + const rev = Math.round(prev); + const growth = Math.round((rand() * 20 - 5) * 10) / 10; + prev = prev * (1 + growth / 100); + return { Month: m, Revenue: rev, Growth: growth }; + }); + tests.push({ + title: 'CJS: Combo — Revenue (bars) + Growth % (line) *', + description: 'Dual-axis combo: bars on the left axis, a % line on the right axis.', + tags: ['chartjs', 'combo', 'dual-axis', 'bar', 'line'], + chartType: 'Combo Chart', + data, + fields: [makeField('Month'), makeField('Revenue'), makeField('Growth')], + metadata: { + Month: { type: Type.String, semanticType: 'Category', levels: months }, + Revenue: { type: Type.Number, semanticType: 'Quantity', levels: [] }, + Growth: { type: Type.Number, semanticType: 'Quantity', levels: [] }, + }, + encodingMap: { x: makeEncodingItem('Month'), y: makeEncodingItem('Revenue') }, + chartProperties: { lineField: 'Growth' }, + }); + } + + // 2. Rainfall (bars) + temperature (line) — classic climograph + { + const rand = seededRandom(59); + const months = genMonths(12); + const data = months.map((m, i) => { + const seasonal = Math.sin((i / 12) * Math.PI * 2); + return { + Month: m, + Rainfall: Math.round(40 + (1 - seasonal) * 60 + rand() * 20), + Temperature: Math.round((15 + seasonal * 10 + rand() * 3) * 10) / 10, + }; + }); + tests.push({ + title: 'CJS: Combo — Rainfall (bars) + Temperature (line) *', + description: 'Climograph-style combo with very different unit scales on each axis.', + tags: ['chartjs', 'combo', 'dual-axis', 'climograph'], + chartType: 'Combo Chart', + data, + fields: [makeField('Month'), makeField('Rainfall'), makeField('Temperature')], + metadata: { + Month: { type: Type.String, semanticType: 'Category', levels: months }, + Rainfall: { type: Type.Number, semanticType: 'Quantity', levels: [] }, + Temperature: { type: Type.Number, semanticType: 'Quantity', levels: [] }, + }, + encodingMap: { x: makeEncodingItem('Month'), y: makeEncodingItem('Rainfall') }, + chartProperties: { lineField: 'Temperature' }, + }); + } + + // 3. Single measure → degrades to a plain bar chart (no line) + { + const rand = seededRandom(61); + const cats = genCategories('Region', 7); + const data = cats.map((c) => ({ Region: c, Sales: Math.round(20 + rand() * 80) })); + tests.push({ + title: 'CJS: Combo — Single Measure (bars only) *', + description: 'No second numeric field; the combo gracefully degrades to bars.', + tags: ['chartjs', 'combo', 'fallback'], + chartType: 'Combo Chart', + data, + fields: [makeField('Region'), makeField('Sales')], + metadata: { + Region: { type: Type.String, semanticType: 'Category', levels: cats }, + Sales: { type: Type.Number, semanticType: 'Quantity', levels: [] }, + }, + encodingMap: { x: makeEncodingItem('Region'), y: makeEncodingItem('Sales') }, + }); + } + + return tests; +} + function genBubbleData(n: number, seed: number, withRegion: boolean) { const rand = seededRandom(seed); const regions = ['Asia', 'Europe', 'Africa', 'Americas']; diff --git a/packages/flint-js/src/test-data/echarts-tests.ts b/packages/flint-js/src/test-data/echarts-tests.ts index 3f7f06bc..6e4a465b 100644 --- a/packages/flint-js/src/test-data/echarts-tests.ts +++ b/packages/flint-js/src/test-data/echarts-tests.ts @@ -2382,3 +2382,171 @@ export function genEChartsParallelTests(): TestCase[] { return tests; } + +// --------------------------------------------------------------------------- +// Network Graph (NEW — flagged with * for inspection) +// --------------------------------------------------------------------------- + +export function genEChartsGraphTests(): TestCase[] { + const tests: TestCase[] = []; + const meta = { + Source: { type: Type.String, semanticType: 'Category', levels: [] as string[] }, + Target: { type: Type.String, semanticType: 'Category', levels: [] as string[] }, + Weight: { type: Type.Number, semanticType: 'Quantity', levels: [] as string[] }, + }; + const fields = [makeField('Source'), makeField('Target'), makeField('Weight')]; + const enc = { x: makeEncodingItem('Source'), y: makeEncodingItem('Target'), size: makeEncodingItem('Weight') }; + + // 1. Small team collaboration network (weighted edges) + { + const people = ['Ana', 'Ben', 'Cara', 'Dan', 'Eve', 'Finn', 'Gia']; + const rand = seededRandom(71); + const data: any[] = []; + for (let i = 0; i < people.length; i++) { + for (let j = i + 1; j < people.length; j++) { + if (rand() < 0.45) { + data.push({ Source: people[i], Target: people[j], Weight: Math.round(1 + rand() * 9) }); + } + } + } + // Guarantee connectivity for a clean render. + if (data.length < 6) { + for (let i = 1; i < people.length; i++) data.push({ Source: people[0], Target: people[i], Weight: 3 }); + } + tests.push({ + title: 'EC: Network Graph — Team Collaboration *', + description: '7 nodes, weighted edges; node size encodes degree, circular layout.', + tags: ['echarts', 'graph', 'network'], + chartType: 'Network Graph', + data, + fields, metadata: meta, encodingMap: enc, + }); + } + + // 2. Hub-and-spoke (one dominant node) + { + const data: any[] = []; + const spokes = ['S1', 'S2', 'S3', 'S4', 'S5', 'S6', 'S7', 'S8']; + const rand = seededRandom(73); + for (const s of spokes) data.push({ Source: 'Hub', Target: s, Weight: Math.round(2 + rand() * 8) }); + // A few peripheral links between spokes. + data.push({ Source: 'S1', Target: 'S2', Weight: 1 }); + data.push({ Source: 'S3', Target: 'S4', Weight: 1 }); + tests.push({ + title: 'EC: Network Graph — Hub & Spoke *', + description: 'A dominant hub connected to 8 spokes; the hub should render largest.', + tags: ['echarts', 'graph', 'network', 'hub'], + chartType: 'Network Graph', + data, + fields, metadata: meta, encodingMap: enc, + }); + } + + // 3. Larger unweighted graph (tests density + sizing) + { + const n = 16; + const nodes = Array.from({ length: n }, (_, i) => `N${i + 1}`); + const rand = seededRandom(79); + const data: any[] = []; + for (let i = 0; i < n; i++) { + for (let j = i + 1; j < n; j++) { + if (rand() < 0.18) data.push({ Source: nodes[i], Target: nodes[j], Weight: 1 }); + } + } + for (let i = 1; i < n; i++) data.push({ Source: nodes[i - 1], Target: nodes[i], Weight: 1 }); + tests.push({ + title: 'EC: Network Graph — 16 Nodes (dense) *', + description: '16 nodes with many unweighted edges; exercises canvas sizing for larger graphs.', + tags: ['echarts', 'graph', 'network', 'dense'], + chartType: 'Network Graph', + data, + fields, metadata: meta, encodingMap: enc, + }); + } + + return tests; +} + +// --------------------------------------------------------------------------- +// Tree (NEW — flagged with * for inspection) +// --------------------------------------------------------------------------- + +export function genEChartsTreeTests(): TestCase[] { + const tests: TestCase[] = []; + + // 1. Two-level org / category tree (color + detail + size) + { + const rand = seededRandom(83); + const groups: Record = { + Engineering: ['Backend', 'Frontend', 'Infra', 'QA'], + Sales: ['EMEA', 'AMER', 'APAC'], + Marketing: ['Brand', 'Growth'], + Support: ['Tier 1', 'Tier 2'], + }; + const data: any[] = []; + for (const [dept, teams] of Object.entries(groups)) { + for (const team of teams) data.push({ Dept: dept, Team: team, Headcount: Math.round(3 + rand() * 20) }); + } + tests.push({ + title: 'EC: Tree — Org Hierarchy *', + description: 'Three levels (root → department → team); leaf value = headcount, left-to-right.', + tags: ['echarts', 'tree', 'hierarchy'], + chartType: 'Tree', + data, + fields: [makeField('Dept'), makeField('Team'), makeField('Headcount')], + metadata: { + Dept: { type: Type.String, semanticType: 'Category', levels: Object.keys(groups) }, + Team: { type: Type.String, semanticType: 'Category', levels: [] }, + Headcount: { type: Type.Number, semanticType: 'Quantity', levels: [] }, + }, + encodingMap: { color: makeEncodingItem('Dept'), detail: makeEncodingItem('Team'), size: makeEncodingItem('Headcount') }, + }); + } + + // 2. Single-level tree (color only → root → categories) + { + const rand = seededRandom(89); + const cats = ['North', 'South', 'East', 'West', 'Central']; + const data = cats.map((c) => ({ Region: c, Sales: Math.round(40 + rand() * 120) })); + tests.push({ + title: 'EC: Tree — Regions (single level) *', + description: 'Two levels (root → region); no detail channel.', + tags: ['echarts', 'tree', 'hierarchy'], + chartType: 'Tree', + data, + fields: [makeField('Region'), makeField('Sales')], + metadata: { + Region: { type: Type.String, semanticType: 'Category', levels: cats }, + Sales: { type: Type.Number, semanticType: 'Quantity', levels: [] }, + }, + encodingMap: { color: makeEncodingItem('Region'), size: makeEncodingItem('Sales') }, + }); + } + + // 3. Many leaves (tall tree → tests vertical sizing) + { + const rand = seededRandom(97); + const groups = ['A', 'B', 'C', 'D']; + const data: any[] = []; + for (const g of groups) { + const count = 5 + Math.floor(rand() * 4); + for (let i = 0; i < count; i++) data.push({ Group: g, Item: `${g}${i + 1}`, Value: Math.round(1 + rand() * 50) }); + } + tests.push({ + title: 'EC: Tree — Many Leaves *', + description: '4 groups with many leaves each; exercises vertical canvas growth.', + tags: ['echarts', 'tree', 'hierarchy', 'dense'], + chartType: 'Tree', + data, + fields: [makeField('Group'), makeField('Item'), makeField('Value')], + metadata: { + Group: { type: Type.String, semanticType: 'Category', levels: groups }, + Item: { type: Type.String, semanticType: 'Category', levels: [] }, + Value: { type: Type.Number, semanticType: 'Quantity', levels: [] }, + }, + encodingMap: { color: makeEncodingItem('Group'), detail: makeEncodingItem('Item'), size: makeEncodingItem('Value') }, + }); + } + + return tests; +} diff --git a/packages/flint-js/src/test-data/index.ts b/packages/flint-js/src/test-data/index.ts index 1bc8f330..0a3ef320 100644 --- a/packages/flint-js/src/test-data/index.ts +++ b/packages/flint-js/src/test-data/index.ts @@ -31,8 +31,8 @@ export { FACET_SIZES, DISCRETE_SIZES, genFacetColumnTests, genFacetRowTests, gen export { genOverflowTests, genElasticityTests } from './stress-tests'; export { genGasPressureTests } from './gas-pressure-tests'; export { genLineAreaStretchTests } from './line-area-stretch-tests'; -export { genEChartsScatterTests, genEChartsLineTests, genEChartsBarTests, genEChartsStackedBarTests, genEChartsGroupedBarTests, genEChartsStressTests, genEChartsAreaTests, genEChartsPieTests, genEChartsHeatmapTests, genEChartsHistogramTests, genEChartsBoxplotTests, genEChartsRadarTests, genEChartsCandlestickTests, genEChartsStreamgraphTests, genEChartsFacetSmallTests, genEChartsFacetWrapTests, genEChartsFacetClipTests, genEChartsRoseTests, genEChartsGaugeTests, genEChartsFunnelTests, genEChartsTreemapTests, genEChartsSunburstTests, genEChartsSankeyTests, genEChartsUniqueStressTests, genEChartsCalendarTests, genEChartsParallelTests } from './echarts-tests'; -export { genChartJsScatterTests, genChartJsLineTests, genChartJsBarTests, genChartJsStackedBarTests, genChartJsGroupedBarTests, genChartJsAreaTests, genChartJsPieTests, genChartJsHistogramTests, genChartJsRadarTests, genChartJsStressTests, genChartJsRoseTests, genChartJsBubbleTests } from './chartjs-tests'; +export { genEChartsScatterTests, genEChartsLineTests, genEChartsBarTests, genEChartsStackedBarTests, genEChartsGroupedBarTests, genEChartsStressTests, genEChartsAreaTests, genEChartsPieTests, genEChartsHeatmapTests, genEChartsHistogramTests, genEChartsBoxplotTests, genEChartsRadarTests, genEChartsCandlestickTests, genEChartsStreamgraphTests, genEChartsFacetSmallTests, genEChartsFacetWrapTests, genEChartsFacetClipTests, genEChartsRoseTests, genEChartsGaugeTests, genEChartsFunnelTests, genEChartsTreemapTests, genEChartsSunburstTests, genEChartsSankeyTests, genEChartsUniqueStressTests, genEChartsCalendarTests, genEChartsParallelTests, genEChartsGraphTests, genEChartsTreeTests } from './echarts-tests'; +export { genChartJsScatterTests, genChartJsLineTests, genChartJsBarTests, genChartJsStackedBarTests, genChartJsGroupedBarTests, genChartJsAreaTests, genChartJsPieTests, genChartJsHistogramTests, genChartJsRadarTests, genChartJsStressTests, genChartJsRoseTests, genChartJsBubbleTests, genChartJsDoughnutTests, genChartJsComboTests } from './chartjs-tests'; export { genGoFishScatterTests, genGoFishLineTests, genGoFishBarTests, genGoFishStackedBarTests, genGoFishGroupedBarTests, genGoFishAreaTests, genGoFishStackedAreaTests, genGoFishPieTests, genGoFishScatterPieTests, genGoFishStressTests } from './gofish-tests'; export { genDiscreteAxisTests } from './discrete-axis-tests'; export { genDateTests, genDateYearTests, genDateMonthTests, genDateYearMonthTests, genDateDecadeTests, genDateDateTimeTests, genDateHoursTests } from './date-tests'; @@ -109,8 +109,8 @@ import { genLineAreaStretchTests } from './line-area-stretch-tests'; import { genDiscreteAxisTests } from './discrete-axis-tests'; import { genDateYearTests, genDateMonthTests, genDateYearMonthTests, genDateDecadeTests, genDateDateTimeTests, genDateHoursTests } from './date-tests'; import { genSemanticContextTests, genSnapToBoundTests } from './semantic-tests'; -import { genEChartsScatterTests, genEChartsLineTests, genEChartsBarTests, genEChartsStackedBarTests, genEChartsGroupedBarTests, genEChartsStressTests, genEChartsAreaTests, genEChartsPieTests, genEChartsHeatmapTests, genEChartsHistogramTests, genEChartsBoxplotTests, genEChartsRadarTests, genEChartsCandlestickTests, genEChartsStreamgraphTests, genEChartsFacetSmallTests, genEChartsFacetWrapTests, genEChartsFacetClipTests, genEChartsRoseTests, genEChartsGaugeTests, genEChartsFunnelTests, genEChartsTreemapTests, genEChartsSunburstTests, genEChartsSankeyTests, genEChartsUniqueStressTests, genEChartsCalendarTests, genEChartsParallelTests } from './echarts-tests'; -import { genChartJsScatterTests, genChartJsLineTests, genChartJsBarTests, genChartJsStackedBarTests, genChartJsGroupedBarTests, genChartJsAreaTests, genChartJsPieTests, genChartJsHistogramTests, genChartJsRadarTests, genChartJsStressTests, genChartJsRoseTests, genChartJsBubbleTests } from './chartjs-tests'; +import { genEChartsScatterTests, genEChartsLineTests, genEChartsBarTests, genEChartsStackedBarTests, genEChartsGroupedBarTests, genEChartsStressTests, genEChartsAreaTests, genEChartsPieTests, genEChartsHeatmapTests, genEChartsHistogramTests, genEChartsBoxplotTests, genEChartsRadarTests, genEChartsCandlestickTests, genEChartsStreamgraphTests, genEChartsFacetSmallTests, genEChartsFacetWrapTests, genEChartsFacetClipTests, genEChartsRoseTests, genEChartsGaugeTests, genEChartsFunnelTests, genEChartsTreemapTests, genEChartsSunburstTests, genEChartsSankeyTests, genEChartsUniqueStressTests, genEChartsCalendarTests, genEChartsParallelTests, genEChartsGraphTests, genEChartsTreeTests } from './echarts-tests'; +import { genChartJsScatterTests, genChartJsLineTests, genChartJsBarTests, genChartJsStackedBarTests, genChartJsGroupedBarTests, genChartJsAreaTests, genChartJsPieTests, genChartJsHistogramTests, genChartJsRadarTests, genChartJsStressTests, genChartJsRoseTests, genChartJsBubbleTests, genChartJsDoughnutTests, genChartJsComboTests } from './chartjs-tests'; import { genGoFishScatterTests, genGoFishLineTests, genGoFishBarTests, genGoFishStackedBarTests, genGoFishGroupedBarTests, genGoFishAreaTests, genGoFishStackedAreaTests, genGoFishPieTests, genGoFishScatterPieTests, genGoFishStressTests } from './gofish-tests'; import { genGalleryRegionalSurveyScatterTests, @@ -207,6 +207,8 @@ export const TEST_GENERATORS: Record TestCase[]> = { 'ECharts: Sankey': genEChartsSankeyTests, 'ECharts: Calendar Heatmap *': genEChartsCalendarTests, 'ECharts: Parallel Coordinates *': genEChartsParallelTests, + 'ECharts: Network Graph *': genEChartsGraphTests, + 'ECharts: Tree *': genEChartsTreeTests, 'ECharts: Unique Stress': genEChartsUniqueStressTests, 'Chart.js: Scatter': genChartJsScatterTests, 'Chart.js: Line': genChartJsLineTests, @@ -219,6 +221,8 @@ export const TEST_GENERATORS: Record TestCase[]> = { 'Chart.js: Radar': genChartJsRadarTests, 'Chart.js: Rose': genChartJsRoseTests, 'Chart.js: Bubble *': genChartJsBubbleTests, + 'Chart.js: Doughnut *': genChartJsDoughnutTests, + 'Chart.js: Combo *': genChartJsComboTests, 'Chart.js: Stress Tests': genChartJsStressTests, 'Gallery: Scatter': genGalleryRegionalSurveyScatterTests, 'Gallery: Line': genGalleryRegionalSurveyLineTests, diff --git a/recursive/chartjs-testing/test_cases/combo_climograph.json b/recursive/chartjs-testing/test_cases/combo_climograph.json new file mode 100644 index 00000000..db5490c5 --- /dev/null +++ b/recursive/chartjs-testing/test_cases/combo_climograph.json @@ -0,0 +1,93 @@ +{ + "test_id": "combo_climograph", + "description": "CJS: Combo — Rainfall (bars) + Temperature (line) *", + "input": { + "data": { + "values": [ + { + "Month": "Jan", + "Rainfall": 100, + "Temperature": 17.3 + }, + { + "Month": "Feb", + "Rainfall": 82, + "Temperature": 20.2 + }, + { + "Month": "Mar", + "Rainfall": 57, + "Temperature": 26.4 + }, + { + "Month": "Apr", + "Rainfall": 56, + "Temperature": 25.2 + }, + { + "Month": "May", + "Rainfall": 50, + "Temperature": 24.1 + }, + { + "Month": "Jun", + "Rainfall": 83, + "Temperature": 21.9 + }, + { + "Month": "Jul", + "Rainfall": 101, + "Temperature": 15.1 + }, + { + "Month": "Aug", + "Rainfall": 133, + "Temperature": 10.8 + }, + { + "Month": "Sep", + "Rainfall": 164, + "Temperature": 7.7 + }, + { + "Month": "Oct", + "Rainfall": 172, + "Temperature": 7.8 + }, + { + "Month": "Nov", + "Rainfall": 165, + "Temperature": 7.9 + }, + { + "Month": "Dec", + "Rainfall": 145, + "Temperature": 12.7 + } + ] + }, + "semantic_types": { + "Month": "Category", + "Rainfall": "Quantity", + "Temperature": "Quantity" + }, + "chart_spec": { + "chartType": "Combo Chart", + "encodings": { + "x": { + "field": "Month" + }, + "y": { + "field": "Rainfall" + } + }, + "canvasSize": { + "width": 480, + "height": 320 + }, + "chartProperties": { + "lineField": "Temperature" + } + } + } +} \ No newline at end of file diff --git a/recursive/chartjs-testing/test_cases/combo_revenue_growth.json b/recursive/chartjs-testing/test_cases/combo_revenue_growth.json new file mode 100644 index 00000000..2a3d69fd --- /dev/null +++ b/recursive/chartjs-testing/test_cases/combo_revenue_growth.json @@ -0,0 +1,93 @@ +{ + "test_id": "combo_revenue_growth", + "description": "CJS: Combo — Revenue (bars) + Growth % (line) *", + "input": { + "data": { + "values": [ + { + "Month": "Jan", + "Revenue": 100, + "Growth": 14.4 + }, + { + "Month": "Feb", + "Revenue": 114, + "Growth": -4.1 + }, + { + "Month": "Mar", + "Revenue": 110, + "Growth": 1.2 + }, + { + "Month": "Apr", + "Revenue": 111, + "Growth": -0.3 + }, + { + "Month": "May", + "Revenue": 111, + "Growth": 7.1 + }, + { + "Month": "Jun", + "Revenue": 119, + "Growth": 4.9 + }, + { + "Month": "Jul", + "Revenue": 124, + "Growth": 14.6 + }, + { + "Month": "Aug", + "Revenue": 143, + "Growth": -4.9 + }, + { + "Month": "Sep", + "Revenue": 136, + "Growth": 5.8 + }, + { + "Month": "Oct", + "Revenue": 143, + "Growth": 1.5 + }, + { + "Month": "Nov", + "Revenue": 146, + "Growth": 5.6 + }, + { + "Month": "Dec", + "Revenue": 154, + "Growth": -4.2 + } + ] + }, + "semantic_types": { + "Month": "Category", + "Revenue": "Quantity", + "Growth": "Quantity" + }, + "chart_spec": { + "chartType": "Combo Chart", + "encodings": { + "x": { + "field": "Month" + }, + "y": { + "field": "Revenue" + } + }, + "canvasSize": { + "width": 480, + "height": 320 + }, + "chartProperties": { + "lineField": "Growth" + } + } + } +} \ No newline at end of file diff --git a/recursive/chartjs-testing/test_cases/combo_single_measure.json b/recursive/chartjs-testing/test_cases/combo_single_measure.json new file mode 100644 index 00000000..9c6798da --- /dev/null +++ b/recursive/chartjs-testing/test_cases/combo_single_measure.json @@ -0,0 +1,57 @@ +{ + "test_id": "combo_single_measure", + "description": "CJS: Combo — Single Measure (bars only) *", + "input": { + "data": { + "values": [ + { + "Region": "Electronics", + "Sales": 20 + }, + { + "Region": "Clothing", + "Sales": 22 + }, + { + "Region": "Food", + "Sales": 27 + }, + { + "Region": "Books", + "Sales": 98 + }, + { + "Region": "Sports", + "Sales": 60 + }, + { + "Region": "Home", + "Sales": 49 + }, + { + "Region": "Garden", + "Sales": 90 + } + ] + }, + "semantic_types": { + "Region": "Category", + "Sales": "Quantity" + }, + "chart_spec": { + "chartType": "Combo Chart", + "encodings": { + "x": { + "field": "Region" + }, + "y": { + "field": "Sales" + } + }, + "canvasSize": { + "width": 480, + "height": 320 + } + } + } +} \ No newline at end of file diff --git a/recursive/chartjs-testing/test_cases/doughnut_browser_share.json b/recursive/chartjs-testing/test_cases/doughnut_browser_share.json new file mode 100644 index 00000000..b5f717a2 --- /dev/null +++ b/recursive/chartjs-testing/test_cases/doughnut_browser_share.json @@ -0,0 +1,49 @@ +{ + "test_id": "doughnut_browser_share", + "description": "CJS: Doughnut — Browser Share *", + "input": { + "data": { + "values": [ + { + "Browser": "Chrome", + "Share": 5 + }, + { + "Browser": "Safari", + "Share": 22.7 + }, + { + "Browser": "Edge", + "Share": 49.1 + }, + { + "Browser": "Firefox", + "Share": 41.2 + }, + { + "Browser": "Other", + "Share": 43 + } + ] + }, + "semantic_types": { + "Browser": "Category", + "Share": "Quantity" + }, + "chart_spec": { + "chartType": "Doughnut Chart", + "encodings": { + "color": { + "field": "Browser" + }, + "size": { + "field": "Share" + } + }, + "canvasSize": { + "width": 480, + "height": 320 + } + } + } +} \ No newline at end of file diff --git a/recursive/chartjs-testing/test_cases/doughnut_counts.json b/recursive/chartjs-testing/test_cases/doughnut_counts.json new file mode 100644 index 00000000..7aeedc8d --- /dev/null +++ b/recursive/chartjs-testing/test_cases/doughnut_counts.json @@ -0,0 +1,205 @@ +{ + "test_id": "doughnut_counts", + "description": "CJS: Doughnut — Issue Types (counts) *", + "input": { + "data": { + "values": [ + { + "Type": "Bug" + }, + { + "Type": "Chore" + }, + { + "Type": "Chore" + }, + { + "Type": "Feature" + }, + { + "Type": "Chore" + }, + { + "Type": "Chore" + }, + { + "Type": "Bug" + }, + { + "Type": "Bug" + }, + { + "Type": "Bug" + }, + { + "Type": "Bug" + }, + { + "Type": "Chore" + }, + { + "Type": "Chore" + }, + { + "Type": "Docs" + }, + { + "Type": "Docs" + }, + { + "Type": "Bug" + }, + { + "Type": "Chore" + }, + { + "Type": "Feature" + }, + { + "Type": "Feature" + }, + { + "Type": "Docs" + }, + { + "Type": "Docs" + }, + { + "Type": "Bug" + }, + { + "Type": "Feature" + }, + { + "Type": "Docs" + }, + { + "Type": "Bug" + }, + { + "Type": "Chore" + }, + { + "Type": "Docs" + }, + { + "Type": "Bug" + }, + { + "Type": "Feature" + }, + { + "Type": "Chore" + }, + { + "Type": "Docs" + }, + { + "Type": "Chore" + }, + { + "Type": "Docs" + }, + { + "Type": "Docs" + }, + { + "Type": "Feature" + }, + { + "Type": "Bug" + }, + { + "Type": "Docs" + }, + { + "Type": "Feature" + }, + { + "Type": "Chore" + }, + { + "Type": "Chore" + }, + { + "Type": "Feature" + }, + { + "Type": "Chore" + }, + { + "Type": "Docs" + }, + { + "Type": "Docs" + }, + { + "Type": "Docs" + }, + { + "Type": "Feature" + }, + { + "Type": "Feature" + }, + { + "Type": "Bug" + }, + { + "Type": "Chore" + }, + { + "Type": "Bug" + }, + { + "Type": "Bug" + }, + { + "Type": "Feature" + }, + { + "Type": "Feature" + }, + { + "Type": "Docs" + }, + { + "Type": "Chore" + }, + { + "Type": "Docs" + }, + { + "Type": "Bug" + }, + { + "Type": "Chore" + }, + { + "Type": "Chore" + }, + { + "Type": "Feature" + }, + { + "Type": "Feature" + } + ] + }, + "semantic_types": { + "Type": "Category" + }, + "chart_spec": { + "chartType": "Doughnut Chart", + "encodings": { + "color": { + "field": "Type" + } + }, + "canvasSize": { + "width": 480, + "height": 320 + } + } + } +} \ No newline at end of file diff --git a/recursive/chartjs-testing/test_cases/doughnut_many_slices.json b/recursive/chartjs-testing/test_cases/doughnut_many_slices.json new file mode 100644 index 00000000..2c2d4c10 --- /dev/null +++ b/recursive/chartjs-testing/test_cases/doughnut_many_slices.json @@ -0,0 +1,65 @@ +{ + "test_id": "doughnut_many_slices", + "description": "CJS: Doughnut — Budget by Department (9 slices) *", + "input": { + "data": { + "values": [ + { + "Dept": "Electronics", + "Budget": 98 + }, + { + "Dept": "Clothing", + "Budget": 94 + }, + { + "Dept": "Food", + "Budget": 85 + }, + { + "Dept": "Books", + "Budget": 87 + }, + { + "Dept": "Sports", + "Budget": 39 + }, + { + "Dept": "Home", + "Budget": 75 + }, + { + "Dept": "Garden", + "Budget": 37 + }, + { + "Dept": "Auto", + "Budget": 16 + }, + { + "Dept": "Health", + "Budget": 48 + } + ] + }, + "semantic_types": { + "Dept": "Category", + "Budget": "Quantity" + }, + "chart_spec": { + "chartType": "Doughnut Chart", + "encodings": { + "color": { + "field": "Dept" + }, + "size": { + "field": "Budget" + } + }, + "canvasSize": { + "width": 480, + "height": 320 + } + } + } +} \ No newline at end of file diff --git a/recursive/chartjs-testing/test_cases/manifest.json b/recursive/chartjs-testing/test_cases/manifest.json index 06dd50fd..4491e1e9 100644 --- a/recursive/chartjs-testing/test_cases/manifest.json +++ b/recursive/chartjs-testing/test_cases/manifest.json @@ -10,5 +10,29 @@ { "test_id": "bubble_dense", "description": "Bubble, dense 120 points, 4 groups" + }, + { + "test_id": "doughnut_browser_share", + "description": "CJS: Doughnut — Browser Share *" + }, + { + "test_id": "doughnut_many_slices", + "description": "CJS: Doughnut — Budget by Department (9 slices) *" + }, + { + "test_id": "doughnut_counts", + "description": "CJS: Doughnut — Issue Types (counts) *" + }, + { + "test_id": "combo_revenue_growth", + "description": "CJS: Combo — Revenue (bars) + Growth % (line) *" + }, + { + "test_id": "combo_climograph", + "description": "CJS: Combo — Rainfall (bars) + Temperature (line) *" + }, + { + "test_id": "combo_single_measure", + "description": "CJS: Combo — Single Measure (bars only) *" } ] \ No newline at end of file diff --git a/recursive/echarts-testing/test_cases/graph_dense_16.json b/recursive/echarts-testing/test_cases/graph_dense_16.json new file mode 100644 index 00000000..79f777f2 --- /dev/null +++ b/recursive/echarts-testing/test_cases/graph_dense_16.json @@ -0,0 +1,188 @@ +{ + "test_id": "graph_dense_16", + "description": "EC: Network Graph — 16 Nodes (dense) *", + "input": { + "data": { + "values": [ + { + "Source": "N1", + "Target": "N2", + "Weight": 1 + }, + { + "Source": "N1", + "Target": "N6", + "Weight": 1 + }, + { + "Source": "N1", + "Target": "N13", + "Weight": 1 + }, + { + "Source": "N2", + "Target": "N4", + "Weight": 1 + }, + { + "Source": "N3", + "Target": "N9", + "Weight": 1 + }, + { + "Source": "N4", + "Target": "N5", + "Weight": 1 + }, + { + "Source": "N5", + "Target": "N7", + "Weight": 1 + }, + { + "Source": "N6", + "Target": "N7", + "Weight": 1 + }, + { + "Source": "N6", + "Target": "N8", + "Weight": 1 + }, + { + "Source": "N6", + "Target": "N9", + "Weight": 1 + }, + { + "Source": "N7", + "Target": "N11", + "Weight": 1 + }, + { + "Source": "N8", + "Target": "N11", + "Weight": 1 + }, + { + "Source": "N10", + "Target": "N15", + "Weight": 1 + }, + { + "Source": "N10", + "Target": "N16", + "Weight": 1 + }, + { + "Source": "N11", + "Target": "N12", + "Weight": 1 + }, + { + "Source": "N12", + "Target": "N14", + "Weight": 1 + }, + { + "Source": "N1", + "Target": "N2", + "Weight": 1 + }, + { + "Source": "N2", + "Target": "N3", + "Weight": 1 + }, + { + "Source": "N3", + "Target": "N4", + "Weight": 1 + }, + { + "Source": "N4", + "Target": "N5", + "Weight": 1 + }, + { + "Source": "N5", + "Target": "N6", + "Weight": 1 + }, + { + "Source": "N6", + "Target": "N7", + "Weight": 1 + }, + { + "Source": "N7", + "Target": "N8", + "Weight": 1 + }, + { + "Source": "N8", + "Target": "N9", + "Weight": 1 + }, + { + "Source": "N9", + "Target": "N10", + "Weight": 1 + }, + { + "Source": "N10", + "Target": "N11", + "Weight": 1 + }, + { + "Source": "N11", + "Target": "N12", + "Weight": 1 + }, + { + "Source": "N12", + "Target": "N13", + "Weight": 1 + }, + { + "Source": "N13", + "Target": "N14", + "Weight": 1 + }, + { + "Source": "N14", + "Target": "N15", + "Weight": 1 + }, + { + "Source": "N15", + "Target": "N16", + "Weight": 1 + } + ] + }, + "semantic_types": { + "Source": "Category", + "Target": "Category", + "Weight": "Quantity" + }, + "chart_spec": { + "chartType": "Network Graph", + "encodings": { + "x": { + "field": "Source" + }, + "y": { + "field": "Target" + }, + "size": { + "field": "Weight" + } + }, + "canvasSize": { + "width": 480, + "height": 320 + } + } + } +} \ No newline at end of file diff --git a/recursive/echarts-testing/test_cases/graph_hub_spoke.json b/recursive/echarts-testing/test_cases/graph_hub_spoke.json new file mode 100644 index 00000000..46e6bbe7 --- /dev/null +++ b/recursive/echarts-testing/test_cases/graph_hub_spoke.json @@ -0,0 +1,83 @@ +{ + "test_id": "graph_hub_spoke", + "description": "EC: Network Graph — Hub & Spoke *", + "input": { + "data": { + "values": [ + { + "Source": "Hub", + "Target": "S1", + "Weight": 2 + }, + { + "Source": "Hub", + "Target": "S2", + "Weight": 7 + }, + { + "Source": "Hub", + "Target": "S3", + "Weight": 3 + }, + { + "Source": "Hub", + "Target": "S4", + "Weight": 6 + }, + { + "Source": "Hub", + "Target": "S5", + "Weight": 9 + }, + { + "Source": "Hub", + "Target": "S6", + "Weight": 10 + }, + { + "Source": "Hub", + "Target": "S7", + "Weight": 5 + }, + { + "Source": "Hub", + "Target": "S8", + "Weight": 6 + }, + { + "Source": "S1", + "Target": "S2", + "Weight": 1 + }, + { + "Source": "S3", + "Target": "S4", + "Weight": 1 + } + ] + }, + "semantic_types": { + "Source": "Category", + "Target": "Category", + "Weight": "Quantity" + }, + "chart_spec": { + "chartType": "Network Graph", + "encodings": { + "x": { + "field": "Source" + }, + "y": { + "field": "Target" + }, + "size": { + "field": "Weight" + } + }, + "canvasSize": { + "width": 480, + "height": 320 + } + } + } +} \ No newline at end of file diff --git a/recursive/echarts-testing/test_cases/graph_team.json b/recursive/echarts-testing/test_cases/graph_team.json new file mode 100644 index 00000000..6284d9de --- /dev/null +++ b/recursive/echarts-testing/test_cases/graph_team.json @@ -0,0 +1,73 @@ +{ + "test_id": "graph_team", + "description": "EC: Network Graph — Team Collaboration *", + "input": { + "data": { + "values": [ + { + "Source": "Ana", + "Target": "Ben", + "Weight": 4 + }, + { + "Source": "Ana", + "Target": "Gia", + "Weight": 3 + }, + { + "Source": "Ben", + "Target": "Cara", + "Weight": 4 + }, + { + "Source": "Ben", + "Target": "Dan", + "Weight": 9 + }, + { + "Source": "Cara", + "Target": "Gia", + "Weight": 8 + }, + { + "Source": "Eve", + "Target": "Finn", + "Weight": 2 + }, + { + "Source": "Eve", + "Target": "Gia", + "Weight": 6 + }, + { + "Source": "Finn", + "Target": "Gia", + "Weight": 6 + } + ] + }, + "semantic_types": { + "Source": "Category", + "Target": "Category", + "Weight": "Quantity" + }, + "chart_spec": { + "chartType": "Network Graph", + "encodings": { + "x": { + "field": "Source" + }, + "y": { + "field": "Target" + }, + "size": { + "field": "Weight" + } + }, + "canvasSize": { + "width": 480, + "height": 320 + } + } + } +} \ No newline at end of file diff --git a/recursive/echarts-testing/test_cases/manifest.json b/recursive/echarts-testing/test_cases/manifest.json index cfc30169..6189a92c 100644 --- a/recursive/echarts-testing/test_cases/manifest.json +++ b/recursive/echarts-testing/test_cases/manifest.json @@ -77,7 +77,7 @@ }, { "test_id": "grouped_bar_basic", - "description": "Grouped bar with 4 cats \u00d7 3 groups" + "description": "Grouped bar with 4 cats × 3 groups" }, { "test_id": "grouped_bar_legend_stress", @@ -85,15 +85,15 @@ }, { "test_id": "grouped_bar_many", - "description": "Grouped bar with 6 cats \u00d7 5 groups (crowding test)" + "description": "Grouped bar with 6 cats × 5 groups (crowding test)" }, { "test_id": "heatmap_basic", - "description": "Basic heatmap 6\u00d75" + "description": "Basic heatmap 6×5" }, { "test_id": "heatmap_large", - "description": "Large heatmap 12\u00d710" + "description": "Large heatmap 12×10" }, { "test_id": "heatmap_sparse", @@ -197,7 +197,7 @@ }, { "test_id": "stacked_bar_basic", - "description": "Stacked bar with 5 cats \u00d7 3 groups" + "description": "Stacked bar with 5 cats × 3 groups" }, { "test_id": "stacked_bar_many_groups", @@ -246,5 +246,29 @@ { "test_id": "par_metrics6", "description": "Parallel coords, 6 dims, 120 rows, 4 groups (hairball test)" + }, + { + "test_id": "graph_team", + "description": "EC: Network Graph — Team Collaboration *" + }, + { + "test_id": "graph_hub_spoke", + "description": "EC: Network Graph — Hub & Spoke *" + }, + { + "test_id": "graph_dense_16", + "description": "EC: Network Graph — 16 Nodes (dense) *" + }, + { + "test_id": "tree_org", + "description": "EC: Tree — Org Hierarchy *" + }, + { + "test_id": "tree_regions", + "description": "EC: Tree — Regions (single level) *" + }, + { + "test_id": "tree_many_leaves", + "description": "EC: Tree — Many Leaves *" } ] \ No newline at end of file diff --git a/recursive/echarts-testing/test_cases/tree_many_leaves.json b/recursive/echarts-testing/test_cases/tree_many_leaves.json new file mode 100644 index 00000000..788bbe3e --- /dev/null +++ b/recursive/echarts-testing/test_cases/tree_many_leaves.json @@ -0,0 +1,148 @@ +{ + "test_id": "tree_many_leaves", + "description": "EC: Tree — Many Leaves *", + "input": { + "data": { + "values": [ + { + "Group": "A", + "Item": "A1", + "Value": 39 + }, + { + "Group": "A", + "Item": "A2", + "Value": 16 + }, + { + "Group": "A", + "Item": "A3", + "Value": 25 + }, + { + "Group": "A", + "Item": "A4", + "Value": 35 + }, + { + "Group": "A", + "Item": "A5", + "Value": 13 + }, + { + "Group": "B", + "Item": "B1", + "Value": 43 + }, + { + "Group": "B", + "Item": "B2", + "Value": 46 + }, + { + "Group": "B", + "Item": "B3", + "Value": 34 + }, + { + "Group": "B", + "Item": "B4", + "Value": 11 + }, + { + "Group": "B", + "Item": "B5", + "Value": 20 + }, + { + "Group": "B", + "Item": "B6", + "Value": 31 + }, + { + "Group": "B", + "Item": "B7", + "Value": 19 + }, + { + "Group": "C", + "Item": "C1", + "Value": 20 + }, + { + "Group": "C", + "Item": "C2", + "Value": 6 + }, + { + "Group": "C", + "Item": "C3", + "Value": 38 + }, + { + "Group": "C", + "Item": "C4", + "Value": 11 + }, + { + "Group": "C", + "Item": "C5", + "Value": 25 + }, + { + "Group": "D", + "Item": "D1", + "Value": 32 + }, + { + "Group": "D", + "Item": "D2", + "Value": 8 + }, + { + "Group": "D", + "Item": "D3", + "Value": 14 + }, + { + "Group": "D", + "Item": "D4", + "Value": 5 + }, + { + "Group": "D", + "Item": "D5", + "Value": 7 + }, + { + "Group": "D", + "Item": "D6", + "Value": 47 + } + ] + }, + "semantic_types": { + "Group": "Category", + "Item": "Category", + "Value": "Quantity" + }, + "chart_spec": { + "chartType": "Tree", + "encodings": { + "color": { + "field": "Group" + }, + "detail": { + "field": "Item" + }, + "size": { + "field": "Value" + } + }, + "canvasSize": { + "width": 480, + "height": 320 + } + } + } +} \ No newline at end of file diff --git a/recursive/echarts-testing/test_cases/tree_org.json b/recursive/echarts-testing/test_cases/tree_org.json new file mode 100644 index 00000000..238c571a --- /dev/null +++ b/recursive/echarts-testing/test_cases/tree_org.json @@ -0,0 +1,88 @@ +{ + "test_id": "tree_org", + "description": "EC: Tree — Org Hierarchy *", + "input": { + "data": { + "values": [ + { + "Dept": "Engineering", + "Team": "Backend", + "Headcount": 3 + }, + { + "Dept": "Engineering", + "Team": "Frontend", + "Headcount": 21 + }, + { + "Dept": "Engineering", + "Team": "Infra", + "Headcount": 17 + }, + { + "Dept": "Engineering", + "Team": "QA", + "Headcount": 4 + }, + { + "Dept": "Sales", + "Team": "EMEA", + "Headcount": 7 + }, + { + "Dept": "Sales", + "Team": "AMER", + "Headcount": 6 + }, + { + "Dept": "Sales", + "Team": "APAC", + "Headcount": 21 + }, + { + "Dept": "Marketing", + "Team": "Brand", + "Headcount": 10 + }, + { + "Dept": "Marketing", + "Team": "Growth", + "Headcount": 11 + }, + { + "Dept": "Support", + "Team": "Tier 1", + "Headcount": 15 + }, + { + "Dept": "Support", + "Team": "Tier 2", + "Headcount": 20 + } + ] + }, + "semantic_types": { + "Dept": "Category", + "Team": "Category", + "Headcount": "Quantity" + }, + "chart_spec": { + "chartType": "Tree", + "encodings": { + "color": { + "field": "Dept" + }, + "detail": { + "field": "Team" + }, + "size": { + "field": "Headcount" + } + }, + "canvasSize": { + "width": 480, + "height": 320 + } + } + } +} \ No newline at end of file diff --git a/recursive/echarts-testing/test_cases/tree_regions.json b/recursive/echarts-testing/test_cases/tree_regions.json new file mode 100644 index 00000000..f23dcf81 --- /dev/null +++ b/recursive/echarts-testing/test_cases/tree_regions.json @@ -0,0 +1,49 @@ +{ + "test_id": "tree_regions", + "description": "EC: Tree — Regions (single level) *", + "input": { + "data": { + "values": [ + { + "Region": "North", + "Sales": 40 + }, + { + "Region": "South", + "Sales": 125 + }, + { + "Region": "East", + "Sales": 70 + }, + { + "Region": "West", + "Sales": 138 + }, + { + "Region": "Central", + "Sales": 90 + } + ] + }, + "semantic_types": { + "Region": "Category", + "Sales": "Quantity" + }, + "chart_spec": { + "chartType": "Tree", + "encodings": { + "color": { + "field": "Region" + }, + "size": { + "field": "Sales" + } + }, + "canvasSize": { + "width": 480, + "height": 320 + } + } + } +} \ No newline at end of file diff --git a/site/src/shared/chart-categories.ts b/site/src/shared/chart-categories.ts index d11de170..3f285c88 100644 --- a/site/src/shared/chart-categories.ts +++ b/site/src/shared/chart-categories.ts @@ -116,7 +116,9 @@ export const CHART_CATEGORIES: ChartCategory[] = [ createChart('echarts', 'echarts-funnel', 'Funnel', 'ECharts: Funnel', funnelIcon), createChart('echarts', 'echarts-treemap', 'Treemap', 'ECharts: Treemap', treemapIcon), createChart('echarts', 'echarts-sunburst', 'Sunburst', 'ECharts: Sunburst', sunburstIcon), + createChart('echarts', 'echarts-tree', 'Tree *', 'ECharts: Tree *', treemapIcon), createChart('echarts', 'echarts-sankey', 'Sankey', 'ECharts: Sankey', sankeyIcon), + createChart('echarts', 'echarts-graph', 'Network Graph *', 'ECharts: Network Graph *', sankeyIcon), ], }, { @@ -128,10 +130,12 @@ export const CHART_CATEGORIES: ChartCategory[] = [ createChart('chartjs', 'chartjs-bubble', 'Bubble Chart *', 'Chart.js: Bubble *', scatterIcon), createChart('chartjs', 'chartjs-line', 'Line Chart', 'Chart.js: Line', lineIcon), createChart('chartjs', 'chartjs-bar', 'Bar Chart', 'Chart.js: Bar', barIcon), + createChart('chartjs', 'chartjs-combo', 'Combo Chart *', 'Chart.js: Combo *', barIcon), createChart('chartjs', 'chartjs-stacked-bar', 'Stacked Bar Chart', 'Chart.js: Stacked Bar', stackedBarIcon), createChart('chartjs', 'chartjs-grouped-bar', 'Grouped Bar Chart', 'Chart.js: Grouped Bar', groupedBarIcon), createChart('chartjs', 'chartjs-area', 'Area Chart', 'Chart.js: Area', areaIcon), createChart('chartjs', 'chartjs-pie', 'Pie Chart', 'Chart.js: Pie', pieIcon), + createChart('chartjs', 'chartjs-doughnut', 'Doughnut Chart *', 'Chart.js: Doughnut *', pieIcon), createChart('chartjs', 'chartjs-histogram', 'Histogram', 'Chart.js: Histogram', histogramIcon), createChart('chartjs', 'chartjs-radar', 'Radar Chart', 'Chart.js: Radar', radarIcon), createChart('chartjs', 'chartjs-rose', 'Rose Chart', 'Chart.js: Rose', roseIcon), From d891b50eb13ca0bbafe48054fad33ae666f2d369 Mon Sep 17 00:00:00 2001 From: Copilot Date: Fri, 12 Jun 2026 17:05:05 +0000 Subject: [PATCH 2/2] feat(gallery): add dedicated icons for Doughnut, Combo, Tree & Network Graph Replace the borrowed sidebar icons for the new templates with purpose-built ones, matching the existing flat blue/grey icon style: - Doughnut: pie wedges with a hollow centre - Combo: bars overlaid with a trend line - Tree: root branching left-to-right into leaves (dendrogram) - Network Graph: nodes connected by edges Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../assets/chart-icons/chart-icon-combo.svg | 18 ++++++++++++++++ .../chart-icons/chart-icon-doughnut.svg | 9 ++++++++ .../assets/chart-icons/chart-icon-network.svg | 19 +++++++++++++++++ .../assets/chart-icons/chart-icon-tree.svg | 21 +++++++++++++++++++ site/src/shared/chart-categories.ts | 12 +++++++---- 5 files changed, 75 insertions(+), 4 deletions(-) create mode 100644 site/src/assets/chart-icons/chart-icon-combo.svg create mode 100644 site/src/assets/chart-icons/chart-icon-doughnut.svg create mode 100644 site/src/assets/chart-icons/chart-icon-network.svg create mode 100644 site/src/assets/chart-icons/chart-icon-tree.svg diff --git a/site/src/assets/chart-icons/chart-icon-combo.svg b/site/src/assets/chart-icons/chart-icon-combo.svg new file mode 100644 index 00000000..24a8705c --- /dev/null +++ b/site/src/assets/chart-icons/chart-icon-combo.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/site/src/assets/chart-icons/chart-icon-doughnut.svg b/site/src/assets/chart-icons/chart-icon-doughnut.svg new file mode 100644 index 00000000..7965de47 --- /dev/null +++ b/site/src/assets/chart-icons/chart-icon-doughnut.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/site/src/assets/chart-icons/chart-icon-network.svg b/site/src/assets/chart-icons/chart-icon-network.svg new file mode 100644 index 00000000..799bb231 --- /dev/null +++ b/site/src/assets/chart-icons/chart-icon-network.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/site/src/assets/chart-icons/chart-icon-tree.svg b/site/src/assets/chart-icons/chart-icon-tree.svg new file mode 100644 index 00000000..77b2157d --- /dev/null +++ b/site/src/assets/chart-icons/chart-icon-tree.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/site/src/shared/chart-categories.ts b/site/src/shared/chart-categories.ts index 0d5797ed..e675284d 100644 --- a/site/src/shared/chart-categories.ts +++ b/site/src/shared/chart-categories.ts @@ -30,6 +30,10 @@ import rangedDotPlotIcon from '../assets/chart-icons/chart-icon-dot-plot-horizon import calendarIcon from '../assets/chart-icons/chart-icon-calendar.svg'; import parallelIcon from '../assets/chart-icons/chart-icon-parallel.svg'; import bubbleIcon from '../assets/chart-icons/chart-icon-bubble.svg'; +import doughnutIcon from '../assets/chart-icons/chart-icon-doughnut.svg'; +import comboIcon from '../assets/chart-icons/chart-icon-combo.svg'; +import treeIcon from '../assets/chart-icons/chart-icon-tree.svg'; +import networkIcon from '../assets/chart-icons/chart-icon-network.svg'; export interface ChartEntry { id: string; @@ -119,9 +123,9 @@ export const CHART_CATEGORIES: ChartCategory[] = [ createChart('echarts', 'echarts-funnel', 'Funnel', 'ECharts: Funnel', funnelIcon), createChart('echarts', 'echarts-treemap', 'Treemap', 'ECharts: Treemap', treemapIcon), createChart('echarts', 'echarts-sunburst', 'Sunburst', 'ECharts: Sunburst', sunburstIcon), - createChart('echarts', 'echarts-tree', 'Tree *', 'ECharts: Tree *', treemapIcon), + createChart('echarts', 'echarts-tree', 'Tree *', 'ECharts: Tree *', treeIcon), createChart('echarts', 'echarts-sankey', 'Sankey', 'ECharts: Sankey', sankeyIcon), - createChart('echarts', 'echarts-graph', 'Network Graph *', 'ECharts: Network Graph *', sankeyIcon), + createChart('echarts', 'echarts-graph', 'Network Graph *', 'ECharts: Network Graph *', networkIcon), ], }, { @@ -133,12 +137,12 @@ export const CHART_CATEGORIES: ChartCategory[] = [ createChart('chartjs', 'chartjs-bubble', 'Bubble Chart *', 'Chart.js: Bubble *', bubbleIcon), createChart('chartjs', 'chartjs-line', 'Line Chart', 'Chart.js: Line', lineIcon), createChart('chartjs', 'chartjs-bar', 'Bar Chart', 'Chart.js: Bar', barIcon), - createChart('chartjs', 'chartjs-combo', 'Combo Chart *', 'Chart.js: Combo *', barIcon), + createChart('chartjs', 'chartjs-combo', 'Combo Chart *', 'Chart.js: Combo *', comboIcon), createChart('chartjs', 'chartjs-stacked-bar', 'Stacked Bar Chart', 'Chart.js: Stacked Bar', stackedBarIcon), createChart('chartjs', 'chartjs-grouped-bar', 'Grouped Bar Chart', 'Chart.js: Grouped Bar', groupedBarIcon), createChart('chartjs', 'chartjs-area', 'Area Chart', 'Chart.js: Area', areaIcon), createChart('chartjs', 'chartjs-pie', 'Pie Chart', 'Chart.js: Pie', pieIcon), - createChart('chartjs', 'chartjs-doughnut', 'Doughnut Chart *', 'Chart.js: Doughnut *', pieIcon), + createChart('chartjs', 'chartjs-doughnut', 'Doughnut Chart *', 'Chart.js: Doughnut *', doughnutIcon), createChart('chartjs', 'chartjs-histogram', 'Histogram', 'Chart.js: Histogram', histogramIcon), createChart('chartjs', 'chartjs-radar', 'Radar Chart', 'Chart.js: Radar', radarIcon), createChart('chartjs', 'chartjs-rose', 'Rose Chart', 'Chart.js: Rose', roseIcon),