From 849d0716e233d0bd75282d641efc184c7b6d155a Mon Sep 17 00:00:00 2001 From: Copilot Date: Fri, 12 Jun 2026 03:07:51 +0000 Subject: [PATCH 1/3] feat(templates): add Bubble (Chart.js), Calendar Heatmap & Parallel Coordinates (ECharts) Add three new, commonly useful chart templates following existing template conventions. New gallery items are flagged with "*" so they are easy to spot for inspection. New templates - Chart.js Bubble Chart: native `bubble` type; the `size` channel maps to an area-proportional, density-aware pixel radius; optional `color` grouping with a right-hand legend. - ECharts Calendar Heatmap: `calendar` coordinate + `heatmap` series with a continuous visualMap; auto-sizes the canvas from the date range (quarter, full year, multi-year). - ECharts Parallel Coordinates: `parallel` coordinate with one axis per numeric dimension; optional categorical color grouping; opacity eases with row count to avoid a hairball. Wiring - Register templates in the chartjs/echarts template registries. - Add gallery test-data generators and TEST_GENERATORS keys ("Chart.js: Bubble *", "ECharts: Calendar Heatmap *", "ECharts: Parallel Coordinates *"). - Add gallery navigation entries (labels flagged with "*"). Verification tooling - Add a Chart.js offline render harness (recursive/chartjs-testing) mirroring the ECharts one, plus a generator->harness converter (recursive/gen_gallery_cases.mts). Each template was rendered to PNG and visually graded across dev + gallery datasets, then iterated. typecheck, lint (0 errors), 51/51 tests and site build all pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../flint-js/src/chartjs/templates/bubble.ts | 184 ++ .../flint-js/src/chartjs/templates/index.ts | 3 +- .../src/echarts/templates/calendar.ts | 186 ++ .../flint-js/src/echarts/templates/index.ts | 6 +- .../src/echarts/templates/parallel.ts | 157 ++ .../flint-js/src/test-data/chartjs-tests.ts | 97 + .../flint-js/src/test-data/echarts-tests.ts | 168 +- packages/flint-js/src/test-data/index.ts | 11 +- recursive/chartjs-testing/batch_render.mts | 116 + .../test_cases/bubble_dense.json | 757 ++++++ .../test_cases/bubble_grouped.json | 277 ++ .../test_cases/bubble_single.json | 233 ++ .../chartjs-testing/test_cases/manifest.json | 14 + .../test_cases/cal_multiyear.json | 2229 +++++++++++++++++ .../test_cases/cal_quarter.json | 509 ++++ .../echarts-testing/test_cases/cal_year.json | 1489 +++++++++++ .../echarts-testing/test_cases/manifest.json | 24 + .../test_cases/par_cars_grouped.json | 344 +++ .../test_cases/par_cars_single.json | 294 +++ .../test_cases/par_metrics6.json | 1111 ++++++++ recursive/gen_gallery_cases.mts | 88 + site/src/shared/chart-categories.ts | 3 + 22 files changed, 8292 insertions(+), 8 deletions(-) create mode 100644 packages/flint-js/src/chartjs/templates/bubble.ts create mode 100644 packages/flint-js/src/echarts/templates/calendar.ts create mode 100644 packages/flint-js/src/echarts/templates/parallel.ts create mode 100644 recursive/chartjs-testing/batch_render.mts create mode 100644 recursive/chartjs-testing/test_cases/bubble_dense.json create mode 100644 recursive/chartjs-testing/test_cases/bubble_grouped.json create mode 100644 recursive/chartjs-testing/test_cases/bubble_single.json create mode 100644 recursive/chartjs-testing/test_cases/manifest.json create mode 100644 recursive/echarts-testing/test_cases/cal_multiyear.json create mode 100644 recursive/echarts-testing/test_cases/cal_quarter.json create mode 100644 recursive/echarts-testing/test_cases/cal_year.json create mode 100644 recursive/echarts-testing/test_cases/par_cars_grouped.json create mode 100644 recursive/echarts-testing/test_cases/par_cars_single.json create mode 100644 recursive/echarts-testing/test_cases/par_metrics6.json create mode 100644 recursive/gen_gallery_cases.mts diff --git a/packages/flint-js/src/chartjs/templates/bubble.ts b/packages/flint-js/src/chartjs/templates/bubble.ts new file mode 100644 index 00000000..2a3e7c97 --- /dev/null +++ b/packages/flint-js/src/chartjs/templates/bubble.ts @@ -0,0 +1,184 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** + * Chart.js Bubble Chart template. + * + * Contrast with VL / Scatter: + * VL: encoding.x + encoding.y + encoding.size (quantitative) → point area + * CJS: native type 'bubble' with data = [{x, y, r}, ...] where `r` is a + * per-point pixel radius derived (area-proportionally) from the size + * channel. Optional color channel groups points into datasets. + */ + +import { ChartTemplateDef } from '../../core/types'; +import { + getChartJsPalette, + getSeriesBorderColor, + getSeriesBackgroundColor, +} from './utils'; + +/** + * Build an area-proportional value → pixel-radius mapper for the size channel. + * Returns a constant-radius mapper when there is no usable size field. + */ +function makeRadiusScale( + values: number[], + rMin: number, + rMax: number, +): (v: number) => number { + const finite = values.filter((v) => typeof v === 'number' && isFinite(v)); + if (finite.length === 0) { + const mid = Math.round((rMin + rMax) / 2); + return () => mid; + } + const min = Math.min(...finite); + const max = Math.max(...finite); + if (min === max) { + const mid = Math.round((rMin + rMax) / 2); + return () => mid; + } + // Area-proportional: interpolate area (r^2) linearly, then take sqrt. + const aMin = rMin * rMin; + const aMax = rMax * rMax; + return (v: number) => { + if (typeof v !== 'number' || !isFinite(v)) return rMin; + const t = (v - min) / (max - min); + const area = aMin + t * (aMax - aMin); + return Math.max(rMin, Math.sqrt(area)); + }; +} + +export const cjsBubbleChartDef: ChartTemplateDef = { + chart: 'Bubble Chart', + template: { mark: 'circle', encoding: {} }, + channels: ['x', 'y', 'size', 'color', 'opacity', 'column', 'row'], + markCognitiveChannel: 'position', + instantiate: (spec, ctx) => { + const { channelSemantics, table, chartProperties } = ctx; + const xField = channelSemantics.x?.field; + const yField = channelSemantics.y?.field; + const sizeField = channelSemantics.size?.field; + const colorField = channelSemantics.color?.field; + + if (!xField || !yField) return; + + const opacity = chartProperties?.opacity ?? 0.6; + const palette = getChartJsPalette(ctx, 'color'); + + // Radius scale spans the whole dataset so groups stay comparable. + const sizeValues = sizeField + ? table.map((row) => Number(row[sizeField])) + : []; + // Provisional radius range; postProcess refines rMax to canvas size. + const radiusScale = makeRadiusScale(sizeValues, 5, 24); + + const toPoint = (row: any) => { + const v = sizeField ? Number(row[sizeField]) : NaN; + return { + x: Number(row[xField]), + y: Number(row[yField]), + r: sizeField ? radiusScale(v) : 8, + // Raw size value retained so postProcess can rescale to canvas. + _v: v, + }; + }; + + const config: any = { + type: 'bubble', + data: { datasets: [] }, + options: { + responsive: true, + maintainAspectRatio: false, + scales: { + x: { type: 'linear', title: { display: true, text: xField } }, + y: { type: 'linear', title: { display: true, text: yField } }, + }, + plugins: { + tooltip: { enabled: true }, + }, + }, + }; + + if (channelSemantics.x?.zero) { + config.options.scales.x.beginAtZero = channelSemantics.x.zero.zero !== false; + } + if (channelSemantics.y?.zero) { + config.options.scales.y.beginAtZero = channelSemantics.y.zero.zero !== false; + } + + if (colorField) { + const groups = new Map(); + for (const row of table) { + const key = String(row[colorField] ?? ''); + if (!groups.has(key)) groups.set(key, []); + groups.get(key)!.push(toPoint(row)); + } + let colorIdx = 0; + for (const [name, data] of groups) { + config.data.datasets.push({ + label: name, + data, + backgroundColor: getSeriesBackgroundColor(palette, colorIdx, opacity), + borderColor: getSeriesBorderColor(palette, colorIdx), + borderWidth: 1, + }); + colorIdx++; + } + config.options.plugins.legend = { display: true }; + } else { + config.data.datasets.push({ + data: table.map(toPoint), + backgroundColor: getSeriesBackgroundColor(palette, 0, opacity), + borderColor: getSeriesBorderColor(palette, 0), + borderWidth: 1, + }); + config.options.plugins.legend = { display: false }; + } + + // Stash size metadata so postProcess can rescale radii to the final canvas. + config._sizeField = sizeField ?? null; + + Object.assign(spec, config); + delete spec.mark; + delete spec.encoding; + }, + properties: [ + { key: 'opacity', label: 'Opacity', type: 'continuous', min: 0.1, max: 1, step: 0.05, defaultValue: 0.6 }, + ], + postProcess: (option, ctx) => { + if (!option.data?.datasets) return; + const sizeField: string | null = option._sizeField ?? null; + delete option._sizeField; + if (!sizeField) { + // Strip helper field even when no size channel is present. + for (const ds of option.data.datasets) for (const pt of ds.data) delete pt._v; + return; + } + + // Collect all raw size values to build a dataset-wide radius scale. + const allValues: number[] = []; + for (const ds of option.data.datasets) { + for (const pt of ds.data) allValues.push(pt._v); + } + + // Scale the max bubble radius to the canvas AND point density so dense + // plots don't turn into a blob. + const w = option._width || ctx.canvasSize.width; + const h = option._height || ctx.canvasSize.height; + const minDim = Math.min(w, h); + const count = Math.max(1, allValues.length); + const rMaxByDensity = Math.sqrt((w * h) / count * 0.08); + const rMax = Math.max(8, Math.min(34, Math.round(Math.min(minDim * 0.09, rMaxByDensity)))); + const rMin = Math.max(3, Math.round(rMax * 0.22)); + const radiusScale = makeRadiusScale(allValues, rMin, rMax); + + for (const ds of option.data.datasets) { + for (const pt of ds.data) { + const v = pt._v; + if (typeof v === 'number' && isFinite(v)) pt.r = radiusScale(v); + delete pt._v; + } + } + }, +}; diff --git a/packages/flint-js/src/chartjs/templates/index.ts b/packages/flint-js/src/chartjs/templates/index.ts index 1c28cb1d..b9b3356b 100644 --- a/packages/flint-js/src/chartjs/templates/index.ts +++ b/packages/flint-js/src/chartjs/templates/index.ts @@ -10,6 +10,7 @@ import { ChartTemplateDef } from '../../core/types'; import { cjsScatterPlotDef } from './scatter'; +import { cjsBubbleChartDef } from './bubble'; import { cjsBarChartDef, cjsStackedBarChartDef, cjsGroupedBarChartDef } from './bar'; import { cjsLineChartDef } from './line'; import { cjsAreaChartDef } from './area'; @@ -22,7 +23,7 @@ import { cjsRoseChartDef } from './rose'; * Chart.js chart template definitions, grouped by category. */ export const cjsTemplateDefs: { [key: string]: ChartTemplateDef[] } = { - 'Scatter & Point': [cjsScatterPlotDef], + 'Scatter & Point': [cjsScatterPlotDef, cjsBubbleChartDef], 'Bar': [cjsBarChartDef, cjsGroupedBarChartDef, cjsStackedBarChartDef, cjsHistogramDef], 'Line & Area': [cjsLineChartDef, cjsAreaChartDef], 'Part-to-Whole': [cjsPieChartDef], diff --git a/packages/flint-js/src/echarts/templates/calendar.ts b/packages/flint-js/src/echarts/templates/calendar.ts new file mode 100644 index 00000000..bc1b2279 --- /dev/null +++ b/packages/flint-js/src/echarts/templates/calendar.ts @@ -0,0 +1,186 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** + * ECharts Calendar Heatmap template. + * + * Contrast with VL: + * VL: has no first-class calendar; would fake it with rect + computed + * week/day fields. + * EC: a `calendar` coordinate system + a `heatmap` series bound to it + * (coordinateSystem: 'calendar'), plus a continuous `visualMap`. + * + * Encoding: + * x (temporal) → the date of each cell + * color (quantitative) → the cell value (defaults to a count of 1) + */ + +import { ChartTemplateDef, EncodingActionDef } from '../../core/types'; +import { getPaletteForScheme } from '../colormap'; + +/** Sequential color ramps (low → high). Mirrors heatmap's scheme vocabulary. */ +const SCHEME_COLORS: Record = { + viridis: ['#440154', '#3b528b', '#21918c', '#5ec962', '#fde725'], + blues: ['#f7fbff', '#6baed6', '#08519c'], + greens: ['#f7fcf5', '#74c476', '#00441b'], + reds: ['#fff5f0', '#fb6a4a', '#a50f15'], + oranges: ['#fff5eb', '#fd8d3c', '#7f2704'], + purples: ['#fcfbfd', '#9e9ac8', '#3f007d'], + github: ['#ebedf0', '#9be9a8', '#40c463', '#30a14e', '#216e39'], +}; + +/** Coerce a raw cell key into a 'YYYY-MM-DD' string, regardless of upstream temporal conversion. */ +function toDateString(raw: unknown): string | null { + if (raw == null) return null; + let d: Date; + if (raw instanceof Date) { + d = raw; + } else if (typeof raw === 'number' && isFinite(raw)) { + // Treat as epoch (ms if large, seconds otherwise). + d = new Date(raw < 1e12 ? raw * 1000 : raw); + } else { + const s = String(raw).trim(); + d = new Date(s); + if (isNaN(d.getTime())) return null; + } + if (isNaN(d.getTime())) return null; + // Use UTC components so the labelled day matches the source date. + const y = d.getUTCFullYear(); + const m = String(d.getUTCMonth() + 1).padStart(2, '0'); + const day = String(d.getUTCDate()).padStart(2, '0'); + return `${y}-${m}-${day}`; +} + +export const ecCalendarHeatmapDef: ChartTemplateDef = { + chart: 'Calendar Heatmap', + template: { mark: 'rect', encoding: {} }, + channels: ['x', 'color'], + markCognitiveChannel: 'color', + instantiate: (spec, ctx) => { + const { channelSemantics, table, colorDecisions, encodings } = ctx; + const dateField = channelSemantics.x?.field; + const valueField = channelSemantics.color?.field; + if (!dateField) return; + + // Aggregate to one value per calendar day (sum when multiple rows share a date). + const cellMap = new Map(); + for (const row of table) { + const dateStr = toDateString(row[dateField]); + if (!dateStr) continue; + const val = valueField ? (Number(row[valueField]) || 0) : 1; + cellMap.set(dateStr, (cellMap.get(dateStr) ?? 0) + val); + } + + const calData: [string, number][] = []; + let minVal = Infinity; + let maxVal = -Infinity; + let minDate = '9999-12-31'; + let maxDate = '0000-01-01'; + for (const [dateStr, val] of cellMap) { + calData.push([dateStr, val]); + if (val < minVal) minVal = val; + if (val > maxVal) maxVal = val; + if (dateStr < minDate) minDate = dateStr; + if (dateStr > maxDate) maxDate = dateStr; + } + if (calData.length === 0) return; + if (minVal === Infinity) minVal = 0; + if (maxVal === -Infinity) maxVal = 1; + if (minVal === maxVal) maxVal = minVal + 1; + + // ── Layout: weeks span the X axis, weekdays the Y axis ─────────────── + const dayMs = 86400000; + const spanDays = (Date.parse(maxDate) - Date.parse(minDate)) / dayMs; + const weeks = Math.max(1, Math.ceil((spanDays + 8) / 7)); + // Shrink cells when the range is long so the canvas stays reasonable. + const cell = weeks > 60 ? 12 : weeks > 30 ? 15 : 18; + const calLeft = 44; // room for weekday labels + const calRight = 16; + const calTop = 34; // room for the month-label row + const vmHeight = 46; // visualMap bar + gap at the bottom + const gridH = 7 * cell; + + const canvasW = calLeft + weeks * cell + calRight; + const canvasH = calTop + gridH + vmHeight; + + // ── Color scheme ───────────────────────────────────────────────────── + const encScheme = encodings?.color?.scheme; + const userScheme = (encScheme && encScheme !== 'default') ? encScheme : undefined; + const schemeName = userScheme || 'viridis'; + const decision = colorDecisions?.color ?? colorDecisions?.group; + let schemeColors: string[] = SCHEME_COLORS[schemeName] || SCHEME_COLORS.viridis; + if (decision?.schemeId) { + const fromDecision = getPaletteForScheme(decision.schemeId); + if (fromDecision && fromDecision.length > 0) schemeColors = fromDecision; + } + + const option: any = { + tooltip: { + trigger: 'item', + formatter: (params: any) => { + const [date, val] = params.value; + return `${date}
${valueField ?? 'Count'}: ${val}`; + }, + }, + visualMap: { + type: 'continuous', + min: minVal, + max: maxVal, + calculable: true, + orient: 'horizontal', + left: 'center', + bottom: 6, + itemWidth: 12, + itemHeight: 100, + text: ['high', 'low'], + inRange: { color: schemeColors }, + }, + calendar: { + top: calTop, + left: calLeft, + right: calRight, + cellSize: [cell, cell], + range: minDate === maxDate ? minDate : [minDate, maxDate], + orient: 'horizontal', + splitLine: { show: true, lineStyle: { color: '#ccc', width: 1 } }, + itemStyle: { borderWidth: 1, borderColor: '#fff', color: '#f4f4f4' }, + yearLabel: { show: false }, + dayLabel: { firstDay: 1, fontSize: 10, color: '#666' }, + monthLabel: { fontSize: 11, color: '#333' }, + }, + series: [{ + type: 'heatmap', + coordinateSystem: 'calendar', + data: calData, + }], + _width: canvasW, + _height: canvasH, + }; + + Object.assign(spec, option); + delete spec.mark; + delete spec.encoding; + }, + encodingActions: [ + { + key: 'colorScheme', + label: 'Scheme', + isApplicable: (ctx) => !!ctx.encodings.color?.field, + dependencies: ['color'], + control: { + type: 'discrete', options: [ + { value: undefined, label: 'Default (Viridis)' }, + { value: 'viridis', label: 'Viridis' }, + { value: 'github', label: 'GitHub' }, + { value: 'blues', label: 'Blues' }, + { value: 'greens', label: 'Greens' }, + { value: 'reds', label: 'Reds' }, + { value: 'oranges', label: 'Oranges' }, + { value: 'purples', label: 'Purples' }, + ], + }, + get: (enc) => enc.color?.scheme, + set: (enc, value) => ({ ...enc, color: { ...enc.color, scheme: value } }), + }, + ] as EncodingActionDef[], +}; diff --git a/packages/flint-js/src/echarts/templates/index.ts b/packages/flint-js/src/echarts/templates/index.ts index 5cace49f..c80ddafb 100644 --- a/packages/flint-js/src/echarts/templates/index.ts +++ b/packages/flint-js/src/echarts/templates/index.ts @@ -32,6 +32,8 @@ import { ecWaterfallChartDef } from './waterfall'; import { ecPyramidChartDef } from './pyramid'; import { ecRangedDotPlotDef } from './ranged-dot'; import { ecDensityPlotDef } from './density'; +import { ecCalendarHeatmapDef } from './calendar'; +import { ecParallelCoordinatesDef } from './parallel'; /** * ECharts chart template definitions, grouped by category. @@ -39,10 +41,10 @@ import { ecDensityPlotDef } from './density'; */ export const ecTemplateDefs: { [key: string]: ChartTemplateDef[] } = { 'Scatter & Point': [ecScatterPlotDef, ecRegressionDef, ecRangedDotPlotDef, ecBoxplotDef, ecStripPlotDef], - 'Bar': [ecBarChartDef, ecGroupedBarChartDef, ecStackedBarChartDef, ecLollipopChartDef, ecPyramidChartDef, ecHeatmapDef], + 'Bar': [ecBarChartDef, ecGroupedBarChartDef, ecStackedBarChartDef, ecLollipopChartDef, ecPyramidChartDef, ecHeatmapDef, ecCalendarHeatmapDef], 'Line & Area': [ecLineChartDef, ecBumpChartDef, ecAreaChartDef, ecStreamgraphDef], 'Part-to-Whole': [ecPieChartDef, ecFunnelChartDef, ecTreemapDef, ecSunburstDef], - 'Statistical': [ecHistogramDef, ecDensityPlotDef], + 'Statistical': [ecHistogramDef, ecDensityPlotDef, ecParallelCoordinatesDef], 'Financial': [ecCandlestickDef], 'Other': [ecWaterfallChartDef], 'Polar': [ecRadarChartDef, ecRoseChartDef], diff --git a/packages/flint-js/src/echarts/templates/parallel.ts b/packages/flint-js/src/echarts/templates/parallel.ts new file mode 100644 index 00000000..e2952237 --- /dev/null +++ b/packages/flint-js/src/echarts/templates/parallel.ts @@ -0,0 +1,157 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** + * ECharts Parallel Coordinates template. + * + * Contrast with VL: + * VL: no first-class parallel coordinates; would require folding many + * quantitative fields into a long format + custom layering. + * EC: a `parallel` coordinate system with one `parallelAxis` per numeric + * dimension and a `series` of type 'parallel'. Optionally grouped (and + * coloured) by a categorical field via the `color` channel. + * + * Dimensions: every numeric field in the data (excluding the color field), + * unless the host passes an explicit `chartProperties.dimensions` order. + */ + +import { ChartTemplateDef } from '../../core/types'; +import { getChartJsPalette } from '../../chartjs/templates/utils'; + +const DEFAULT_COLORS = [ + '#5470c6', '#91cc75', '#fac858', '#ee6666', '#73c0de', + '#3ba272', '#fc8452', '#9a60b4', '#ea7ccc', '#c0504d', +]; + +/** Numeric-ness test: a field is a dimension if (nearly) all 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 ecParallelCoordinatesDef: ChartTemplateDef = { + chart: 'Parallel Coordinates', + template: { mark: 'line', encoding: {} }, + channels: ['color', 'detail'], + markCognitiveChannel: 'position', + instantiate: (spec, ctx) => { + const { channelSemantics, table, chartProperties } = ctx; + if (table.length === 0) return; + + const colorField = channelSemantics.color?.field; + + // Resolve the ordered list of numeric dimensions. + let dims: string[] = Array.isArray(chartProperties?.dimensions) + ? chartProperties!.dimensions.filter((d: string) => d in table[0]) + : []; + if (dims.length === 0) { + dims = Object.keys(table[0]).filter( + (k) => k !== colorField && isNumericField(table, k), + ); + } + if (dims.length < 2) return; + + const palette = colorField ? getChartJsPalette(ctx, 'color') : DEFAULT_COLORS; + const colors = palette.length > 0 ? palette : DEFAULT_COLORS; + + const parallelAxis = dims.map((name, i) => ({ + dim: i, + name, + nameTextStyle: { fontSize: 11 }, + nameGap: 8, + axisLabel: { fontSize: 10 }, + })); + + const toLine = (row: any) => dims.map((d) => { + const v = Number(row[d]); + return isFinite(v) ? v : null; + }); + + const series: any[] = []; + const legendData: string[] = []; + // Thinner, more transparent lines as the dataset grows (avoid a hairball). + const lineOpacity = table.length > 200 ? 0.22 : table.length > 100 ? 0.3 : table.length > 60 ? 0.45 : 0.6; + + if (colorField) { + const groups = new Map(); + for (const row of table) { + const key = String(row[colorField] ?? ''); + if (!groups.has(key)) groups.set(key, []); + groups.get(key)!.push(toLine(row)); + } + let i = 0; + for (const [name, data] of groups) { + legendData.push(name); + series.push({ + name, + type: 'parallel', + data, + lineStyle: { width: 1.5, opacity: lineOpacity, color: colors[i % colors.length] }, + emphasis: { lineStyle: { width: 3, opacity: 0.9 } }, + }); + i++; + } + } else { + series.push({ + type: 'parallel', + data: table.map(toLine), + lineStyle: { width: 1.5, opacity: lineOpacity, color: colors[0] }, + emphasis: { lineStyle: { width: 3, opacity: 0.9 } }, + }); + } + + const hasLegend = legendData.length > 1; + + // ── Layout: one column band per dimension ──────────────────────────── + const parTop = hasLegend ? 56 : 28; + const parBottom = 36; + const parLeft = 56; // room for the first axis labels + const parRight = 56; // room for the last axis name + const perDim = 96; // horizontal band per dimension + const canvasW = Math.max(ctx.canvasSize.width, parLeft + parRight + (dims.length - 1) * perDim); + const canvasH = Math.max(ctx.canvasSize.height, parTop + parBottom + 200); + + const option: any = { + tooltip: {}, + parallelAxis, + parallel: { + top: parTop, + bottom: parBottom, + left: parLeft, + right: parRight, + parallelAxisDefault: { + nameLocation: 'end', + nameGap: 14, + axisLine: { lineStyle: { color: '#888' } }, + axisLabel: { color: '#555' }, + }, + }, + series, + _width: canvasW, + _height: canvasH, + }; + + if (hasLegend) { + option.legend = { + data: legendData, + top: 8, + left: 'center', + orient: 'horizontal', + itemWidth: 18, + textStyle: { fontSize: 11 }, + ...(legendData.length > 10 ? { type: 'scroll' } : {}), + }; + } + + Object.assign(spec, option); + delete spec.mark; + delete spec.encoding; + }, +}; diff --git a/packages/flint-js/src/test-data/chartjs-tests.ts b/packages/flint-js/src/test-data/chartjs-tests.ts index c495253c..711690f4 100644 --- a/packages/flint-js/src/test-data/chartjs-tests.ts +++ b/packages/flint-js/src/test-data/chartjs-tests.ts @@ -613,3 +613,100 @@ export function genChartJsRoseTests(): TestCase[] { return tests; } + +// --------------------------------------------------------------------------- +// Bubble Chart (NEW — flagged with * for inspection) +// --------------------------------------------------------------------------- + +function genBubbleData(n: number, seed: number, withRegion: boolean) { + const rand = seededRandom(seed); + const regions = ['Asia', 'Europe', 'Africa', 'Americas']; + return Array.from({ length: n }, (_, i) => { + const row: Record = { + GDP: Math.round((1 + rand() * 59) * 10) / 10, + LifeExp: Math.round((50 + rand() * 35) * 10) / 10, + Population: Math.round((1 + rand() * 1400) * 10) / 10, + }; + if (withRegion) row.Region = regions[i % regions.length]; + return row; + }); +} + +export function genChartJsBubbleTests(): TestCase[] { + const tests: TestCase[] = []; + + // 1. Grouped bubble: size + color + { + const data = genBubbleData(40, 17, true); + tests.push({ + title: 'CJS: Bubble — GDP × LifeExp × Population *', + description: 'Bubble chart: x=GDP, y=LifeExp, size=Population, color=Region (4 groups).', + tags: ['chartjs', 'bubble', 'size', 'color'], + chartType: 'Bubble Chart', + data, + fields: [makeField('GDP'), makeField('LifeExp'), makeField('Population'), makeField('Region')], + metadata: { + GDP: { type: Type.Number, semanticType: 'Quantity', levels: [] }, + LifeExp: { type: Type.Number, semanticType: 'Quantity', levels: [] }, + Population: { type: Type.Number, semanticType: 'Quantity', levels: [] }, + Region: { type: Type.String, semanticType: 'Category', levels: ['Asia', 'Europe', 'Africa', 'Americas'] }, + }, + encodingMap: { + x: makeEncodingItem('GDP'), + y: makeEncodingItem('LifeExp'), + size: makeEncodingItem('Population'), + color: makeEncodingItem('Region'), + }, + }); + } + + // 2. Single-series bubble: size only + { + const data = genBubbleData(36, 23, false); + tests.push({ + title: 'CJS: Bubble — Single Series (size only) *', + description: 'Bubble chart with one series; bubble area encodes Population.', + tags: ['chartjs', 'bubble', 'size'], + chartType: 'Bubble Chart', + data, + fields: [makeField('GDP'), makeField('LifeExp'), makeField('Population')], + metadata: { + GDP: { type: Type.Number, semanticType: 'Quantity', levels: [] }, + LifeExp: { type: Type.Number, semanticType: 'Quantity', levels: [] }, + Population: { type: Type.Number, semanticType: 'Quantity', levels: [] }, + }, + encodingMap: { + x: makeEncodingItem('GDP'), + y: makeEncodingItem('LifeExp'), + size: makeEncodingItem('Population'), + }, + }); + } + + // 3. Dense bubble: tests density-aware radius scaling + { + const data = genBubbleData(120, 29, true); + tests.push({ + title: 'CJS: Bubble — Dense (120 points) *', + description: 'Dense bubble chart; radius shrinks with point density to stay legible.', + tags: ['chartjs', 'bubble', 'dense', 'size', 'color'], + chartType: 'Bubble Chart', + data, + fields: [makeField('GDP'), makeField('LifeExp'), makeField('Population'), makeField('Region')], + metadata: { + GDP: { type: Type.Number, semanticType: 'Quantity', levels: [] }, + LifeExp: { type: Type.Number, semanticType: 'Quantity', levels: [] }, + Population: { type: Type.Number, semanticType: 'Quantity', levels: [] }, + Region: { type: Type.String, semanticType: 'Category', levels: ['Asia', 'Europe', 'Africa', 'Americas'] }, + }, + encodingMap: { + x: makeEncodingItem('GDP'), + y: makeEncodingItem('LifeExp'), + size: makeEncodingItem('Population'), + color: makeEncodingItem('Region'), + }, + }); + } + + return tests; +} diff --git a/packages/flint-js/src/test-data/echarts-tests.ts b/packages/flint-js/src/test-data/echarts-tests.ts index ec86de39..3f7f06bc 100644 --- a/packages/flint-js/src/test-data/echarts-tests.ts +++ b/packages/flint-js/src/test-data/echarts-tests.ts @@ -2215,4 +2215,170 @@ export function genEChartsUniqueStressTests(): TestCase[] { } return tests; -} \ No newline at end of file +} +// --------------------------------------------------------------------------- +// Calendar Heatmap (NEW — flagged with * for inspection) +// --------------------------------------------------------------------------- + +function genDailySeries(startISO: string, days: number, seed: number) { + const rand = seededRandom(seed); + const start = new Date(startISO + 'T00:00:00Z'); + return Array.from({ length: days }, (_, i) => { + const d = new Date(start); + d.setUTCDate(start.getUTCDate() + i); + const weekday = d.getUTCDay(); // 0 Sun .. 6 Sat + const base = weekday === 0 || weekday === 6 ? 2 : 8; + const v = Math.max(0, Math.round(base + (rand() - 0.5) * 10)); + return { date: d.toISOString().slice(0, 10), commits: v }; + }); +} + +export function genEChartsCalendarTests(): TestCase[] { + const tests: TestCase[] = []; + const meta = { + date: { type: Type.Date, semanticType: 'Date', levels: [] }, + commits: { type: Type.Number, semanticType: 'Quantity', levels: [] }, + }; + const enc = { x: makeEncodingItem('date'), color: makeEncodingItem('commits') }; + const fields = [makeField('date'), makeField('commits')]; + + // 1. One quarter + tests.push({ + title: 'EC: Calendar Heatmap — One Quarter *', + description: '~120 daily values laid out on a calendar; weekdays run higher than weekends.', + tags: ['echarts', 'calendar', 'heatmap', 'temporal'], + chartType: 'Calendar Heatmap', + data: genDailySeries('2023-01-01', 120, 7), + fields, metadata: meta, encodingMap: enc, + }); + + // 2. Full year + tests.push({ + title: 'EC: Calendar Heatmap — Full Year *', + description: 'A full year of daily activity (365 cells), GitHub-contributions style.', + tags: ['echarts', 'calendar', 'heatmap', 'temporal'], + chartType: 'Calendar Heatmap', + data: genDailySeries('2023-01-01', 365, 11), + fields, metadata: meta, encodingMap: enc, + }); + + // 3. Multi-year span + tests.push({ + title: 'EC: Calendar Heatmap — 18 Months *', + description: 'A range that crosses a year boundary (~550 daily cells).', + tags: ['echarts', 'calendar', 'heatmap', 'temporal', 'multi-year'], + chartType: 'Calendar Heatmap', + data: genDailySeries('2022-06-01', 550, 13), + fields, metadata: meta, encodingMap: enc, + }); + + return tests; +} + +// --------------------------------------------------------------------------- +// Parallel Coordinates (NEW — flagged with * for inspection) +// --------------------------------------------------------------------------- + +function genCarRows(n: number, seed: number, withOrigin: boolean) { + const rand = seededRandom(seed); + const origins = ['USA', 'Europe', 'Japan']; + const gauss = () => (rand() + rand() + rand() - 1.5); // ~N(0,~0.5) + return Array.from({ length: n }, (_, i) => { + const origin = origins[i % origins.length]; + let mpg, hp, wt, acc; + if (origin === 'USA') { mpg = 20 + gauss() * 8; hp = 150 + gauss() * 60; wt = 3400 + gauss() * 800; acc = 15 + gauss() * 4; } + else if (origin === 'Europe') { mpg = 27 + gauss() * 8; hp = 110 + gauss() * 50; wt = 2800 + gauss() * 700; acc = 16 + gauss() * 4; } + else { mpg = 31 + gauss() * 8; hp = 95 + gauss() * 40; wt = 2300 + gauss() * 600; acc = 16.5 + gauss() * 4; } + const row: Record = { + MPG: Math.round(mpg * 10) / 10, + Horsepower: Math.round(hp), + Weight: Math.round(wt), + Acceleration: Math.round(acc * 10) / 10, + }; + if (withOrigin) row.Origin = origin; + return row; + }); +} + +export function genEChartsParallelTests(): TestCase[] { + const tests: TestCase[] = []; + + // 1. Grouped by Origin (4 dims, 3 groups) + { + const data = genCarRows(45, 11, true); + tests.push({ + title: 'EC: Parallel Coordinates — Cars by Origin *', + description: '4 quantitative dimensions, lines colored by Origin (3 groups).', + tags: ['echarts', 'parallel', 'multivariate', 'color'], + chartType: 'Parallel Coordinates', + data, + fields: [makeField('MPG'), makeField('Horsepower'), makeField('Weight'), makeField('Acceleration'), makeField('Origin')], + metadata: { + MPG: { type: Type.Number, semanticType: 'Quantity', levels: [] }, + Horsepower: { type: Type.Number, semanticType: 'Quantity', levels: [] }, + Weight: { type: Type.Number, semanticType: 'Quantity', levels: [] }, + Acceleration: { type: Type.Number, semanticType: 'Quantity', levels: [] }, + Origin: { type: Type.String, semanticType: 'Category', levels: ['USA', 'Europe', 'Japan'] }, + }, + encodingMap: { color: makeEncodingItem('Origin') }, + chartProperties: { dimensions: ['MPG', 'Horsepower', 'Weight', 'Acceleration'] }, + }); + } + + // 2. Single series (no color) + { + const data = genCarRows(40, 17, false); + tests.push({ + title: 'EC: Parallel Coordinates — Single Series *', + description: '4 quantitative dimensions, no grouping.', + tags: ['echarts', 'parallel', 'multivariate'], + chartType: 'Parallel Coordinates', + data, + fields: [makeField('MPG'), makeField('Horsepower'), makeField('Weight'), makeField('Acceleration')], + metadata: { + MPG: { type: Type.Number, semanticType: 'Quantity', levels: [] }, + Horsepower: { type: Type.Number, semanticType: 'Quantity', levels: [] }, + Weight: { type: Type.Number, semanticType: 'Quantity', levels: [] }, + Acceleration: { type: Type.Number, semanticType: 'Quantity', levels: [] }, + }, + encodingMap: {}, + chartProperties: { dimensions: ['MPG', 'Horsepower', 'Weight', 'Acceleration'] }, + }); + } + + // 3. Six dimensions, more rows + { + const rand = seededRandom(31); + const tiers = ['A', 'B', 'C', 'D']; + const data = Array.from({ length: 90 }, (_, i) => ({ + Speed: Math.round(rand() * 1000) / 10, + Power: Math.round(rand() * 1000) / 10, + Range: Math.round(rand() * 1000) / 10, + Comfort: Math.round(rand() * 1000) / 10, + Price: Math.round(rand() * 1000) / 10, + Safety: Math.round(rand() * 1000) / 10, + Tier: tiers[i % tiers.length], + })); + tests.push({ + title: 'EC: Parallel Coordinates — Six Metrics *', + description: '6 quantitative dimensions, 90 rows, 4 groups.', + tags: ['echarts', 'parallel', 'multivariate', 'dense'], + chartType: 'Parallel Coordinates', + data, + fields: [makeField('Speed'), makeField('Power'), makeField('Range'), makeField('Comfort'), makeField('Price'), makeField('Safety'), makeField('Tier')], + metadata: { + Speed: { type: Type.Number, semanticType: 'Quantity', levels: [] }, + Power: { type: Type.Number, semanticType: 'Quantity', levels: [] }, + Range: { type: Type.Number, semanticType: 'Quantity', levels: [] }, + Comfort: { type: Type.Number, semanticType: 'Quantity', levels: [] }, + Price: { type: Type.Number, semanticType: 'Quantity', levels: [] }, + Safety: { type: Type.Number, semanticType: 'Quantity', levels: [] }, + Tier: { type: Type.String, semanticType: 'Category', levels: ['A', 'B', 'C', 'D'] }, + }, + encodingMap: { color: makeEncodingItem('Tier') }, + chartProperties: { dimensions: ['Speed', 'Power', 'Range', 'Comfort', 'Price', 'Safety'] }, + }); + } + + return tests; +} diff --git a/packages/flint-js/src/test-data/index.ts b/packages/flint-js/src/test-data/index.ts index 6ef90ba4..1bc8f330 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 } from './echarts-tests'; -export { genChartJsScatterTests, genChartJsLineTests, genChartJsBarTests, genChartJsStackedBarTests, genChartJsGroupedBarTests, genChartJsAreaTests, genChartJsPieTests, genChartJsHistogramTests, genChartJsRadarTests, genChartJsStressTests, genChartJsRoseTests } 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 } from './echarts-tests'; +export { genChartJsScatterTests, genChartJsLineTests, genChartJsBarTests, genChartJsStackedBarTests, genChartJsGroupedBarTests, genChartJsAreaTests, genChartJsPieTests, genChartJsHistogramTests, genChartJsRadarTests, genChartJsStressTests, genChartJsRoseTests, genChartJsBubbleTests } 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 } from './echarts-tests'; -import { genChartJsScatterTests, genChartJsLineTests, genChartJsBarTests, genChartJsStackedBarTests, genChartJsGroupedBarTests, genChartJsAreaTests, genChartJsPieTests, genChartJsHistogramTests, genChartJsRadarTests, genChartJsStressTests, genChartJsRoseTests } 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 } from './echarts-tests'; +import { genChartJsScatterTests, genChartJsLineTests, genChartJsBarTests, genChartJsStackedBarTests, genChartJsGroupedBarTests, genChartJsAreaTests, genChartJsPieTests, genChartJsHistogramTests, genChartJsRadarTests, genChartJsStressTests, genChartJsRoseTests, genChartJsBubbleTests } from './chartjs-tests'; import { genGoFishScatterTests, genGoFishLineTests, genGoFishBarTests, genGoFishStackedBarTests, genGoFishGroupedBarTests, genGoFishAreaTests, genGoFishStackedAreaTests, genGoFishPieTests, genGoFishScatterPieTests, genGoFishStressTests } from './gofish-tests'; import { genGalleryRegionalSurveyScatterTests, @@ -205,6 +205,8 @@ export const TEST_GENERATORS: Record TestCase[]> = { 'ECharts: Treemap': genEChartsTreemapTests, 'ECharts: Sunburst': genEChartsSunburstTests, 'ECharts: Sankey': genEChartsSankeyTests, + 'ECharts: Calendar Heatmap *': genEChartsCalendarTests, + 'ECharts: Parallel Coordinates *': genEChartsParallelTests, 'ECharts: Unique Stress': genEChartsUniqueStressTests, 'Chart.js: Scatter': genChartJsScatterTests, 'Chart.js: Line': genChartJsLineTests, @@ -216,6 +218,7 @@ export const TEST_GENERATORS: Record TestCase[]> = { 'Chart.js: Histogram': genChartJsHistogramTests, 'Chart.js: Radar': genChartJsRadarTests, 'Chart.js: Rose': genChartJsRoseTests, + 'Chart.js: Bubble *': genChartJsBubbleTests, 'Chart.js: Stress Tests': genChartJsStressTests, 'Gallery: Scatter': genGalleryRegionalSurveyScatterTests, 'Gallery: Line': genGalleryRegionalSurveyLineTests, diff --git a/recursive/chartjs-testing/batch_render.mts b/recursive/chartjs-testing/batch_render.mts new file mode 100644 index 00000000..2d6e48be --- /dev/null +++ b/recursive/chartjs-testing/batch_render.mts @@ -0,0 +1,116 @@ +/** + * Batch render all Chart.js test cases. + * + * Reads test_cases/*.json, runs assembleChartjs on each, renders to PNG via + * @napi-rs/canvas, and saves results next to a _spec.json dump. + * + * Usage: npx tsx recursive/chartjs-testing/batch_render.mts + * + * Must run from repo root. + */ +import { assembleChartjs } from '../../packages/flint-js/src/chartjs/assemble'; +import { createRequire } from 'module'; +import * as fs from 'fs'; +import * as path from 'path'; +import { fileURLToPath } from 'url'; + +const require = createRequire(import.meta.url); +const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..'); + +// Chart.js from site/node_modules (v4); canvas from root node_modules. +const napiCanvas = require(path.join(REPO_ROOT, 'node_modules/@napi-rs/canvas')); +const { createCanvas } = napiCanvas; +const chartjs = require(path.join(REPO_ROOT, 'site/node_modules/chart.js/auto')); +const Chart = chartjs.default || chartjs.Chart || chartjs; + +// Register system fonts for proper text rendering. +if (napiCanvas.GlobalFonts) { + const fontPaths = [ + '/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf', + '/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf', + ]; + for (const fp of fontPaths) { + if (fs.existsSync(fp)) napiCanvas.GlobalFonts.registerFromPath(fp, 'sans-serif'); + } +} + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const TEST_DIR = path.join(__dirname, 'test_cases'); +const OUTPUT_DIR = path.join(__dirname, 'rendered'); + +function asFinite(v: unknown): number | undefined { + return typeof v === 'number' && isFinite(v) ? v : undefined; +} + +function renderOne(testId: string, input: any): { width: number; height: number } { + const config = assembleChartjs(input); + const width = asFinite(config._width) ?? input.chart_spec?.canvasSize?.width ?? 480; + const height = asFinite(config._height) ?? input.chart_spec?.canvasSize?.height ?? 320; + + const canvas: any = createCanvas(width, height); + canvas.style = {}; + + const merged = { + ...config, + options: { + ...(config.options ?? {}), + responsive: false, + maintainAspectRatio: false, + animation: false, + devicePixelRatio: 1, + }, + }; + // Strip flint-internal hints so Chart.js doesn't choke. + delete (merged as any)._width; + delete (merged as any)._height; + delete (merged as any)._warnings; + delete (merged as any)._dataLength; + + const chart = new Chart(canvas, merged); + chart.draw(); + const buf = canvas.toBuffer('image/png'); + fs.writeFileSync(path.join(OUTPUT_DIR, `${testId}.png`), buf); + fs.writeFileSync( + path.join(OUTPUT_DIR, `${testId}_spec.json`), + JSON.stringify(config, null, 2), + ); + chart.destroy(); + return { width, height }; +} + +function main() { + if (!fs.existsSync(OUTPUT_DIR)) fs.mkdirSync(OUTPUT_DIR, { recursive: true }); + + const manifestPath = path.join(TEST_DIR, 'manifest.json'); + let entries: { test_id: string; description?: string }[]; + if (fs.existsSync(manifestPath)) { + entries = JSON.parse(fs.readFileSync(manifestPath, 'utf-8')); + } else { + entries = fs.readdirSync(TEST_DIR) + .filter((f) => f.endsWith('.json') && f !== 'manifest.json') + .map((f) => ({ test_id: f.replace(/\.json$/, '') })); + } + + let success = 0; + let failed = 0; + for (const entry of entries) { + const file = path.join(TEST_DIR, `${entry.test_id}.json`); + if (!fs.existsSync(file)) { + console.error(`❌ ${entry.test_id} (missing file)`); + failed++; + continue; + } + try { + const tc = JSON.parse(fs.readFileSync(file, 'utf-8')); + const { width, height } = renderOne(entry.test_id, tc.input); + console.log(`✅ ${entry.test_id} (${width}×${height})`); + success++; + } catch (err) { + console.error(`❌ ${entry.test_id}:`, (err as Error).message); + failed++; + } + } + console.log(`\n=== Done: ${success} success, ${failed} failed out of ${entries.length} ===`); +} + +main(); diff --git a/recursive/chartjs-testing/test_cases/bubble_dense.json b/recursive/chartjs-testing/test_cases/bubble_dense.json new file mode 100644 index 00000000..2c43085e --- /dev/null +++ b/recursive/chartjs-testing/test_cases/bubble_dense.json @@ -0,0 +1,757 @@ +{ + "test_id": "bubble_dense", + "description": "Bubble, dense 120 points, 4 groups", + "input": { + "data": { + "values": [ + { + "GDP": 47.3, + "LifeExp": 68.2, + "Population": 716.6, + "Region": "Asia" + }, + { + "GDP": 21.5, + "LifeExp": 68.5, + "Population": 819.0, + "Region": "Americas" + }, + { + "GDP": 7.4, + "LifeExp": 64.4, + "Population": 1355.2, + "Region": "Africa" + }, + { + "GDP": 46.6, + "LifeExp": 81.4, + "Population": 33.0, + "Region": "Asia" + }, + { + "GDP": 52.9, + "LifeExp": 82.2, + "Population": 160.3, + "Region": "Europe" + }, + { + "GDP": 23.9, + "LifeExp": 75.7, + "Population": 1124.5, + "Region": "Americas" + }, + { + "GDP": 2.0, + "LifeExp": 70.3, + "Population": 593.0, + "Region": "Americas" + }, + { + "GDP": 27.0, + "LifeExp": 84.6, + "Population": 799.5, + "Region": "Americas" + }, + { + "GDP": 47.7, + "LifeExp": 83.4, + "Population": 326.7, + "Region": "Europe" + }, + { + "GDP": 48.3, + "LifeExp": 63.7, + "Population": 194.8, + "Region": "Asia" + }, + { + "GDP": 7.2, + "LifeExp": 74.7, + "Population": 10.7, + "Region": "Americas" + }, + { + "GDP": 54.4, + "LifeExp": 70.8, + "Population": 728.7, + "Region": "Americas" + }, + { + "GDP": 22.8, + "LifeExp": 81.4, + "Population": 1212.6, + "Region": "Americas" + }, + { + "GDP": 15.4, + "LifeExp": 50.9, + "Population": 527.9, + "Region": "Asia" + }, + { + "GDP": 40.9, + "LifeExp": 72.8, + "Population": 1309.6, + "Region": "Asia" + }, + { + "GDP": 17.7, + "LifeExp": 64.1, + "Population": 1029.8, + "Region": "Americas" + }, + { + "GDP": 26.9, + "LifeExp": 60.8, + "Population": 1071.9, + "Region": "Americas" + }, + { + "GDP": 13.3, + "LifeExp": 74.1, + "Population": 1053.1, + "Region": "Asia" + }, + { + "GDP": 28.3, + "LifeExp": 66.4, + "Population": 632.3, + "Region": "Asia" + }, + { + "GDP": 11.3, + "LifeExp": 72.7, + "Population": 106.5, + "Region": "Asia" + }, + { + "GDP": 11.1, + "LifeExp": 60.0, + "Population": 654.4, + "Region": "Europe" + }, + { + "GDP": 13.0, + "LifeExp": 66.4, + "Population": 1397.4, + "Region": "Asia" + }, + { + "GDP": 26.3, + "LifeExp": 83.1, + "Population": 215.3, + "Region": "Asia" + }, + { + "GDP": 37.6, + "LifeExp": 60.1, + "Population": 892.2, + "Region": "Americas" + }, + { + "GDP": 34.6, + "LifeExp": 70.6, + "Population": 1341.9, + "Region": "Asia" + }, + { + "GDP": 16.6, + "LifeExp": 61.1, + "Population": 35.4, + "Region": "Europe" + }, + { + "GDP": 39.6, + "LifeExp": 57.4, + "Population": 276.7, + "Region": "Africa" + }, + { + "GDP": 45.4, + "LifeExp": 54.8, + "Population": 453.4, + "Region": "Americas" + }, + { + "GDP": 34.2, + "LifeExp": 75.1, + "Population": 1033.3, + "Region": "Africa" + }, + { + "GDP": 38.8, + "LifeExp": 73.8, + "Population": 400.4, + "Region": "Africa" + }, + { + "GDP": 49.7, + "LifeExp": 82.7, + "Population": 1220.5, + "Region": "Americas" + }, + { + "GDP": 10.1, + "LifeExp": 66.0, + "Population": 1302.1, + "Region": "Europe" + }, + { + "GDP": 16.4, + "LifeExp": 66.3, + "Population": 531.6, + "Region": "Asia" + }, + { + "GDP": 20.4, + "LifeExp": 56.5, + "Population": 1204.1, + "Region": "Africa" + }, + { + "GDP": 58.3, + "LifeExp": 53.3, + "Population": 935.1, + "Region": "Africa" + }, + { + "GDP": 25.4, + "LifeExp": 51.4, + "Population": 507.2, + "Region": "Americas" + }, + { + "GDP": 26.8, + "LifeExp": 66.0, + "Population": 1023.7, + "Region": "Africa" + }, + { + "GDP": 18.5, + "LifeExp": 59.6, + "Population": 1368.2, + "Region": "Asia" + }, + { + "GDP": 2.9, + "LifeExp": 56.0, + "Population": 658.4, + "Region": "Americas" + }, + { + "GDP": 42.9, + "LifeExp": 68.0, + "Population": 1223.5, + "Region": "Asia" + }, + { + "GDP": 3.8, + "LifeExp": 73.9, + "Population": 858.8, + "Region": "Americas" + }, + { + "GDP": 33.0, + "LifeExp": 69.1, + "Population": 47.6, + "Region": "Europe" + }, + { + "GDP": 9.8, + "LifeExp": 60.1, + "Population": 825.7, + "Region": "Europe" + }, + { + "GDP": 8.6, + "LifeExp": 74.3, + "Population": 435.6, + "Region": "Americas" + }, + { + "GDP": 35.1, + "LifeExp": 62.4, + "Population": 1099.6, + "Region": "Americas" + }, + { + "GDP": 16.2, + "LifeExp": 62.1, + "Population": 1295.5, + "Region": "Europe" + }, + { + "GDP": 33.8, + "LifeExp": 81.2, + "Population": 84.5, + "Region": "Europe" + }, + { + "GDP": 8.3, + "LifeExp": 56.7, + "Population": 90.2, + "Region": "Americas" + }, + { + "GDP": 23.8, + "LifeExp": 59.8, + "Population": 491.1, + "Region": "Europe" + }, + { + "GDP": 3.9, + "LifeExp": 64.9, + "Population": 807.2, + "Region": "Americas" + }, + { + "GDP": 33.8, + "LifeExp": 53.6, + "Population": 560.2, + "Region": "Africa" + }, + { + "GDP": 41.6, + "LifeExp": 58.1, + "Population": 641.1, + "Region": "Europe" + }, + { + "GDP": 6.1, + "LifeExp": 76.5, + "Population": 614.9, + "Region": "Americas" + }, + { + "GDP": 35.1, + "LifeExp": 84.8, + "Population": 3.8, + "Region": "Asia" + }, + { + "GDP": 50.6, + "LifeExp": 82.9, + "Population": 380.8, + "Region": "Europe" + }, + { + "GDP": 10.7, + "LifeExp": 68.2, + "Population": 368.7, + "Region": "Americas" + }, + { + "GDP": 26.0, + "LifeExp": 50.8, + "Population": 134.0, + "Region": "Americas" + }, + { + "GDP": 48.7, + "LifeExp": 59.4, + "Population": 1098.3, + "Region": "Americas" + }, + { + "GDP": 56.0, + "LifeExp": 84.5, + "Population": 349.4, + "Region": "Americas" + }, + { + "GDP": 46.7, + "LifeExp": 69.1, + "Population": 426.8, + "Region": "Europe" + }, + { + "GDP": 5.3, + "LifeExp": 73.4, + "Population": 1326.9, + "Region": "Asia" + }, + { + "GDP": 4.7, + "LifeExp": 51.2, + "Population": 1147.5, + "Region": "Americas" + }, + { + "GDP": 19.3, + "LifeExp": 84.4, + "Population": 394.4, + "Region": "Americas" + }, + { + "GDP": 8.1, + "LifeExp": 71.3, + "Population": 62.0, + "Region": "Europe" + }, + { + "GDP": 7.9, + "LifeExp": 61.8, + "Population": 493.5, + "Region": "Europe" + }, + { + "GDP": 7.3, + "LifeExp": 84.4, + "Population": 1097.1, + "Region": "Americas" + }, + { + "GDP": 3.0, + "LifeExp": 56.0, + "Population": 1057.0, + "Region": "Europe" + }, + { + "GDP": 19.6, + "LifeExp": 57.4, + "Population": 687.5, + "Region": "Americas" + }, + { + "GDP": 2.5, + "LifeExp": 75.2, + "Population": 680.1, + "Region": "Asia" + }, + { + "GDP": 11.9, + "LifeExp": 72.2, + "Population": 238.9, + "Region": "Europe" + }, + { + "GDP": 47.6, + "LifeExp": 56.6, + "Population": 144.4, + "Region": "Asia" + }, + { + "GDP": 32.3, + "LifeExp": 67.7, + "Population": 290.7, + "Region": "Americas" + }, + { + "GDP": 59.2, + "LifeExp": 68.5, + "Population": 486.6, + "Region": "Asia" + }, + { + "GDP": 19.7, + "LifeExp": 55.3, + "Population": 920.5, + "Region": "Europe" + }, + { + "GDP": 54.6, + "LifeExp": 79.1, + "Population": 59.1, + "Region": "Europe" + }, + { + "GDP": 50.8, + "LifeExp": 79.5, + "Population": 596.5, + "Region": "Africa" + }, + { + "GDP": 41.6, + "LifeExp": 74.4, + "Population": 751.4, + "Region": "Europe" + }, + { + "GDP": 30.5, + "LifeExp": 50.8, + "Population": 796.7, + "Region": "Americas" + }, + { + "GDP": 35.3, + "LifeExp": 68.2, + "Population": 1252.4, + "Region": "Americas" + }, + { + "GDP": 54.4, + "LifeExp": 65.8, + "Population": 1141.8, + "Region": "Africa" + }, + { + "GDP": 6.6, + "LifeExp": 54.6, + "Population": 1190.5, + "Region": "Africa" + }, + { + "GDP": 9.8, + "LifeExp": 74.6, + "Population": 550.0, + "Region": "Americas" + }, + { + "GDP": 15.0, + "LifeExp": 77.4, + "Population": 1042.6, + "Region": "Europe" + }, + { + "GDP": 40.5, + "LifeExp": 66.8, + "Population": 517.7, + "Region": "Africa" + }, + { + "GDP": 31.4, + "LifeExp": 82.2, + "Population": 676.5, + "Region": "Asia" + }, + { + "GDP": 58.6, + "LifeExp": 80.0, + "Population": 1311.3, + "Region": "Europe" + }, + { + "GDP": 10.4, + "LifeExp": 67.1, + "Population": 614.1, + "Region": "Americas" + }, + { + "GDP": 8.5, + "LifeExp": 59.9, + "Population": 460.7, + "Region": "Africa" + }, + { + "GDP": 23.8, + "LifeExp": 84.4, + "Population": 1296.9, + "Region": "Americas" + }, + { + "GDP": 40.5, + "LifeExp": 77.9, + "Population": 771.6, + "Region": "Americas" + }, + { + "GDP": 22.5, + "LifeExp": 84.8, + "Population": 19.3, + "Region": "Europe" + }, + { + "GDP": 40.5, + "LifeExp": 51.2, + "Population": 409.6, + "Region": "Europe" + }, + { + "GDP": 21.7, + "LifeExp": 75.4, + "Population": 1318.1, + "Region": "Asia" + }, + { + "GDP": 32.4, + "LifeExp": 64.2, + "Population": 674.6, + "Region": "Africa" + }, + { + "GDP": 4.9, + "LifeExp": 83.5, + "Population": 495.2, + "Region": "Asia" + }, + { + "GDP": 10.2, + "LifeExp": 60.1, + "Population": 1270.3, + "Region": "Africa" + }, + { + "GDP": 31.5, + "LifeExp": 67.3, + "Population": 255.0, + "Region": "Africa" + }, + { + "GDP": 56.9, + "LifeExp": 67.6, + "Population": 596.1, + "Region": "Africa" + }, + { + "GDP": 38.5, + "LifeExp": 82.8, + "Population": 909.8, + "Region": "Africa" + }, + { + "GDP": 56.2, + "LifeExp": 61.3, + "Population": 49.1, + "Region": "Americas" + }, + { + "GDP": 37.4, + "LifeExp": 79.6, + "Population": 512.7, + "Region": "Europe" + }, + { + "GDP": 57.0, + "LifeExp": 78.8, + "Population": 369.6, + "Region": "Americas" + }, + { + "GDP": 11.4, + "LifeExp": 80.2, + "Population": 976.8, + "Region": "Africa" + }, + { + "GDP": 49.2, + "LifeExp": 78.5, + "Population": 39.7, + "Region": "Europe" + }, + { + "GDP": 35.3, + "LifeExp": 83.4, + "Population": 443.5, + "Region": "Americas" + }, + { + "GDP": 45.8, + "LifeExp": 76.4, + "Population": 435.4, + "Region": "Africa" + }, + { + "GDP": 26.6, + "LifeExp": 61.1, + "Population": 1158.9, + "Region": "Africa" + }, + { + "GDP": 53.2, + "LifeExp": 56.2, + "Population": 786.6, + "Region": "Europe" + }, + { + "GDP": 44.5, + "LifeExp": 59.6, + "Population": 356.5, + "Region": "Americas" + }, + { + "GDP": 42.8, + "LifeExp": 59.6, + "Population": 922.4, + "Region": "Africa" + }, + { + "GDP": 22.5, + "LifeExp": 67.7, + "Population": 1122.6, + "Region": "Asia" + }, + { + "GDP": 18.7, + "LifeExp": 60.1, + "Population": 254.0, + "Region": "Americas" + }, + { + "GDP": 14.4, + "LifeExp": 67.5, + "Population": 81.0, + "Region": "Americas" + }, + { + "GDP": 22.8, + "LifeExp": 62.6, + "Population": 1199.1, + "Region": "Europe" + }, + { + "GDP": 27.8, + "LifeExp": 80.2, + "Population": 836.7, + "Region": "Europe" + }, + { + "GDP": 16.5, + "LifeExp": 77.5, + "Population": 1326.8, + "Region": "Asia" + }, + { + "GDP": 41.4, + "LifeExp": 75.2, + "Population": 608.6, + "Region": "Americas" + }, + { + "GDP": 45.6, + "LifeExp": 53.9, + "Population": 160.7, + "Region": "Europe" + }, + { + "GDP": 46.4, + "LifeExp": 55.4, + "Population": 1110.1, + "Region": "Africa" + }, + { + "GDP": 37.5, + "LifeExp": 63.6, + "Population": 1145.0, + "Region": "Europe" + } + ] + }, + "semantic_types": { + "GDP": "Quantity", + "LifeExp": "Quantity", + "Population": "Quantity", + "Region": "Category" + }, + "chart_spec": { + "chartType": "Bubble Chart", + "encodings": { + "x": { + "field": "GDP" + }, + "y": { + "field": "LifeExp" + }, + "size": { + "field": "Population" + }, + "color": { + "field": "Region" + } + }, + "canvasSize": { + "width": 480, + "height": 320 + } + } + } +} \ No newline at end of file diff --git a/recursive/chartjs-testing/test_cases/bubble_grouped.json b/recursive/chartjs-testing/test_cases/bubble_grouped.json new file mode 100644 index 00000000..485b9915 --- /dev/null +++ b/recursive/chartjs-testing/test_cases/bubble_grouped.json @@ -0,0 +1,277 @@ +{ + "test_id": "bubble_grouped", + "description": "Bubble, x=GDP y=LifeExp size=Population color=Region", + "input": { + "data": { + "values": [ + { + "GDP": 48.6, + "LifeExp": 83.6, + "Population": 406.2, + "Region": "Americas" + }, + { + "GDP": 7.5, + "LifeExp": 50.9, + "Population": 538.5, + "Region": "Africa" + }, + { + "GDP": 15.9, + "LifeExp": 67.6, + "Population": 445.1, + "Region": "Americas" + }, + { + "GDP": 9.1, + "LifeExp": 69.3, + "Population": 196.7, + "Region": "Americas" + }, + { + "GDP": 9.9, + "LifeExp": 74.7, + "Population": 782.9, + "Region": "Europe" + }, + { + "GDP": 20.5, + "LifeExp": 54.3, + "Population": 1002.1, + "Region": "Europe" + }, + { + "GDP": 19.2, + "LifeExp": 52.9, + "Population": 1315.7, + "Region": "Asia" + }, + { + "GDP": 38.8, + "LifeExp": 71.0, + "Population": 956.0, + "Region": "Americas" + }, + { + "GDP": 49.0, + "LifeExp": 82.2, + "Population": 26.0, + "Region": "Americas" + }, + { + "GDP": 34.7, + "LifeExp": 81.4, + "Population": 1034.7, + "Region": "Africa" + }, + { + "GDP": 57.6, + "LifeExp": 63.1, + "Population": 1084.4, + "Region": "Asia" + }, + { + "GDP": 15.1, + "LifeExp": 69.5, + "Population": 1340.4, + "Region": "Americas" + }, + { + "GDP": 4.4, + "LifeExp": 52.2, + "Population": 386.4, + "Region": "Africa" + }, + { + "GDP": 15.8, + "LifeExp": 60.8, + "Population": 487.6, + "Region": "Europe" + }, + { + "GDP": 20.6, + "LifeExp": 81.8, + "Population": 810.1, + "Region": "Europe" + }, + { + "GDP": 20.8, + "LifeExp": 51.8, + "Population": 1386.8, + "Region": "Africa" + }, + { + "GDP": 28.6, + "LifeExp": 59.5, + "Population": 886.7, + "Region": "Asia" + }, + { + "GDP": 19.3, + "LifeExp": 52.6, + "Population": 1381.5, + "Region": "Europe" + }, + { + "GDP": 2.2, + "LifeExp": 69.7, + "Population": 960.7, + "Region": "Americas" + }, + { + "GDP": 29.6, + "LifeExp": 83.3, + "Population": 1370.0, + "Region": "Americas" + }, + { + "GDP": 11.8, + "LifeExp": 84.8, + "Population": 1345.3, + "Region": "Americas" + }, + { + "GDP": 54.5, + "LifeExp": 51.8, + "Population": 1362.6, + "Region": "Asia" + }, + { + "GDP": 48.5, + "LifeExp": 82.2, + "Population": 1135.3, + "Region": "Africa" + }, + { + "GDP": 10.5, + "LifeExp": 64.7, + "Population": 968.2, + "Region": "Asia" + }, + { + "GDP": 50.5, + "LifeExp": 73.0, + "Population": 565.8, + "Region": "Africa" + }, + { + "GDP": 33.0, + "LifeExp": 69.0, + "Population": 1371.0, + "Region": "Asia" + }, + { + "GDP": 32.4, + "LifeExp": 59.3, + "Population": 229.6, + "Region": "Europe" + }, + { + "GDP": 3.9, + "LifeExp": 81.1, + "Population": 821.4, + "Region": "Africa" + }, + { + "GDP": 14.1, + "LifeExp": 64.3, + "Population": 877.9, + "Region": "Americas" + }, + { + "GDP": 58.1, + "LifeExp": 76.3, + "Population": 321.5, + "Region": "Asia" + }, + { + "GDP": 49.4, + "LifeExp": 83.9, + "Population": 837.8, + "Region": "Africa" + }, + { + "GDP": 4.2, + "LifeExp": 79.1, + "Population": 1350.2, + "Region": "Americas" + }, + { + "GDP": 55.9, + "LifeExp": 69.5, + "Population": 858.6, + "Region": "Europe" + }, + { + "GDP": 11.2, + "LifeExp": 84.0, + "Population": 296.3, + "Region": "Africa" + }, + { + "GDP": 22.0, + "LifeExp": 70.7, + "Population": 726.6, + "Region": "Americas" + }, + { + "GDP": 12.3, + "LifeExp": 70.1, + "Population": 985.8, + "Region": "Asia" + }, + { + "GDP": 27.5, + "LifeExp": 65.6, + "Population": 706.5, + "Region": "Europe" + }, + { + "GDP": 27.7, + "LifeExp": 60.3, + "Population": 258.5, + "Region": "Americas" + }, + { + "GDP": 49.8, + "LifeExp": 66.4, + "Population": 1053.0, + "Region": "Asia" + }, + { + "GDP": 4.1, + "LifeExp": 53.4, + "Population": 934.3, + "Region": "Europe" + } + ] + }, + "semantic_types": { + "GDP": "Quantity", + "LifeExp": "Quantity", + "Population": "Quantity", + "Region": "Category" + }, + "chart_spec": { + "chartType": "Bubble Chart", + "encodings": { + "x": { + "field": "GDP" + }, + "y": { + "field": "LifeExp" + }, + "size": { + "field": "Population" + }, + "color": { + "field": "Region" + } + }, + "canvasSize": { + "width": 480, + "height": 320 + } + } + } +} \ No newline at end of file diff --git a/recursive/chartjs-testing/test_cases/bubble_single.json b/recursive/chartjs-testing/test_cases/bubble_single.json new file mode 100644 index 00000000..e36e8264 --- /dev/null +++ b/recursive/chartjs-testing/test_cases/bubble_single.json @@ -0,0 +1,233 @@ +{ + "test_id": "bubble_single", + "description": "Bubble, single series, size encodes Population", + "input": { + "data": { + "values": [ + { + "GDP": 48.6, + "LifeExp": 83.6, + "Population": 406.2 + }, + { + "GDP": 7.5, + "LifeExp": 50.9, + "Population": 538.5 + }, + { + "GDP": 15.9, + "LifeExp": 67.6, + "Population": 445.1 + }, + { + "GDP": 9.1, + "LifeExp": 69.3, + "Population": 196.7 + }, + { + "GDP": 9.9, + "LifeExp": 74.7, + "Population": 782.9 + }, + { + "GDP": 20.5, + "LifeExp": 54.3, + "Population": 1002.1 + }, + { + "GDP": 19.2, + "LifeExp": 52.9, + "Population": 1315.7 + }, + { + "GDP": 38.8, + "LifeExp": 71.0, + "Population": 956.0 + }, + { + "GDP": 49.0, + "LifeExp": 82.2, + "Population": 26.0 + }, + { + "GDP": 34.7, + "LifeExp": 81.4, + "Population": 1034.7 + }, + { + "GDP": 57.6, + "LifeExp": 63.1, + "Population": 1084.4 + }, + { + "GDP": 15.1, + "LifeExp": 69.5, + "Population": 1340.4 + }, + { + "GDP": 4.4, + "LifeExp": 52.2, + "Population": 386.4 + }, + { + "GDP": 15.8, + "LifeExp": 60.8, + "Population": 487.6 + }, + { + "GDP": 20.6, + "LifeExp": 81.8, + "Population": 810.1 + }, + { + "GDP": 20.8, + "LifeExp": 51.8, + "Population": 1386.8 + }, + { + "GDP": 28.6, + "LifeExp": 59.5, + "Population": 886.7 + }, + { + "GDP": 19.3, + "LifeExp": 52.6, + "Population": 1381.5 + }, + { + "GDP": 2.2, + "LifeExp": 69.7, + "Population": 960.7 + }, + { + "GDP": 29.6, + "LifeExp": 83.3, + "Population": 1370.0 + }, + { + "GDP": 11.8, + "LifeExp": 84.8, + "Population": 1345.3 + }, + { + "GDP": 54.5, + "LifeExp": 51.8, + "Population": 1362.6 + }, + { + "GDP": 48.5, + "LifeExp": 82.2, + "Population": 1135.3 + }, + { + "GDP": 10.5, + "LifeExp": 64.7, + "Population": 968.2 + }, + { + "GDP": 50.5, + "LifeExp": 73.0, + "Population": 565.8 + }, + { + "GDP": 33.0, + "LifeExp": 69.0, + "Population": 1371.0 + }, + { + "GDP": 32.4, + "LifeExp": 59.3, + "Population": 229.6 + }, + { + "GDP": 3.9, + "LifeExp": 81.1, + "Population": 821.4 + }, + { + "GDP": 14.1, + "LifeExp": 64.3, + "Population": 877.9 + }, + { + "GDP": 58.1, + "LifeExp": 76.3, + "Population": 321.5 + }, + { + "GDP": 49.4, + "LifeExp": 83.9, + "Population": 837.8 + }, + { + "GDP": 4.2, + "LifeExp": 79.1, + "Population": 1350.2 + }, + { + "GDP": 55.9, + "LifeExp": 69.5, + "Population": 858.6 + }, + { + "GDP": 11.2, + "LifeExp": 84.0, + "Population": 296.3 + }, + { + "GDP": 22.0, + "LifeExp": 70.7, + "Population": 726.6 + }, + { + "GDP": 12.3, + "LifeExp": 70.1, + "Population": 985.8 + }, + { + "GDP": 27.5, + "LifeExp": 65.6, + "Population": 706.5 + }, + { + "GDP": 27.7, + "LifeExp": 60.3, + "Population": 258.5 + }, + { + "GDP": 49.8, + "LifeExp": 66.4, + "Population": 1053.0 + }, + { + "GDP": 4.1, + "LifeExp": 53.4, + "Population": 934.3 + } + ] + }, + "semantic_types": { + "GDP": "Quantity", + "LifeExp": "Quantity", + "Population": "Quantity" + }, + "chart_spec": { + "chartType": "Bubble Chart", + "encodings": { + "x": { + "field": "GDP" + }, + "y": { + "field": "LifeExp" + }, + "size": { + "field": "Population" + } + }, + "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 new file mode 100644 index 00000000..06dd50fd --- /dev/null +++ b/recursive/chartjs-testing/test_cases/manifest.json @@ -0,0 +1,14 @@ +[ + { + "test_id": "bubble_grouped", + "description": "Bubble, x=GDP y=LifeExp size=Population color=Region" + }, + { + "test_id": "bubble_single", + "description": "Bubble, single series, size encodes Population" + }, + { + "test_id": "bubble_dense", + "description": "Bubble, dense 120 points, 4 groups" + } +] \ No newline at end of file diff --git a/recursive/echarts-testing/test_cases/cal_multiyear.json b/recursive/echarts-testing/test_cases/cal_multiyear.json new file mode 100644 index 00000000..8f206f4a --- /dev/null +++ b/recursive/echarts-testing/test_cases/cal_multiyear.json @@ -0,0 +1,2229 @@ +{ + "test_id": "cal_multiyear", + "description": "Calendar heatmap spanning ~18 months across years", + "input": { + "data": { + "values": [ + { + "date": "2022-06-01", + "commits": 9 + }, + { + "date": "2022-06-02", + "commits": 7 + }, + { + "date": "2022-06-03", + "commits": 5 + }, + { + "date": "2022-06-04", + "commits": 1 + }, + { + "date": "2022-06-05", + "commits": 0 + }, + { + "date": "2022-06-06", + "commits": 5 + }, + { + "date": "2022-06-07", + "commits": 8 + }, + { + "date": "2022-06-08", + "commits": 3 + }, + { + "date": "2022-06-09", + "commits": 8 + }, + { + "date": "2022-06-10", + "commits": 8 + }, + { + "date": "2022-06-11", + "commits": 0 + }, + { + "date": "2022-06-12", + "commits": 1 + }, + { + "date": "2022-06-13", + "commits": 7 + }, + { + "date": "2022-06-14", + "commits": 9 + }, + { + "date": "2022-06-15", + "commits": 9 + }, + { + "date": "2022-06-16", + "commits": 7 + }, + { + "date": "2022-06-17", + "commits": 5 + }, + { + "date": "2022-06-18", + "commits": 1 + }, + { + "date": "2022-06-19", + "commits": 1 + }, + { + "date": "2022-06-20", + "commits": 10 + }, + { + "date": "2022-06-21", + "commits": 8 + }, + { + "date": "2022-06-22", + "commits": 5 + }, + { + "date": "2022-06-23", + "commits": 3 + }, + { + "date": "2022-06-24", + "commits": 6 + }, + { + "date": "2022-06-25", + "commits": 0 + }, + { + "date": "2022-06-26", + "commits": 0 + }, + { + "date": "2022-06-27", + "commits": 7 + }, + { + "date": "2022-06-28", + "commits": 6 + }, + { + "date": "2022-06-29", + "commits": 8 + }, + { + "date": "2022-06-30", + "commits": 9 + }, + { + "date": "2022-07-01", + "commits": 6 + }, + { + "date": "2022-07-02", + "commits": 8 + }, + { + "date": "2022-07-03", + "commits": 1 + }, + { + "date": "2022-07-04", + "commits": 11 + }, + { + "date": "2022-07-05", + "commits": 8 + }, + { + "date": "2022-07-06", + "commits": 11 + }, + { + "date": "2022-07-07", + "commits": 0 + }, + { + "date": "2022-07-08", + "commits": 5 + }, + { + "date": "2022-07-09", + "commits": 2 + }, + { + "date": "2022-07-10", + "commits": 3 + }, + { + "date": "2022-07-11", + "commits": 15 + }, + { + "date": "2022-07-12", + "commits": 8 + }, + { + "date": "2022-07-13", + "commits": 11 + }, + { + "date": "2022-07-14", + "commits": 10 + }, + { + "date": "2022-07-15", + "commits": 10 + }, + { + "date": "2022-07-16", + "commits": 3 + }, + { + "date": "2022-07-17", + "commits": 1 + }, + { + "date": "2022-07-18", + "commits": 9 + }, + { + "date": "2022-07-19", + "commits": 4 + }, + { + "date": "2022-07-20", + "commits": 11 + }, + { + "date": "2022-07-21", + "commits": 4 + }, + { + "date": "2022-07-22", + "commits": 8 + }, + { + "date": "2022-07-23", + "commits": 8 + }, + { + "date": "2022-07-24", + "commits": 1 + }, + { + "date": "2022-07-25", + "commits": 8 + }, + { + "date": "2022-07-26", + "commits": 11 + }, + { + "date": "2022-07-27", + "commits": 8 + }, + { + "date": "2022-07-28", + "commits": 5 + }, + { + "date": "2022-07-29", + "commits": 8 + }, + { + "date": "2022-07-30", + "commits": 3 + }, + { + "date": "2022-07-31", + "commits": 4 + }, + { + "date": "2022-08-01", + "commits": 5 + }, + { + "date": "2022-08-02", + "commits": 13 + }, + { + "date": "2022-08-03", + "commits": 13 + }, + { + "date": "2022-08-04", + "commits": 8 + }, + { + "date": "2022-08-05", + "commits": 8 + }, + { + "date": "2022-08-06", + "commits": 0 + }, + { + "date": "2022-08-07", + "commits": 6 + }, + { + "date": "2022-08-08", + "commits": 5 + }, + { + "date": "2022-08-09", + "commits": 10 + }, + { + "date": "2022-08-10", + "commits": 6 + }, + { + "date": "2022-08-11", + "commits": 5 + }, + { + "date": "2022-08-12", + "commits": 10 + }, + { + "date": "2022-08-13", + "commits": 6 + }, + { + "date": "2022-08-14", + "commits": 1 + }, + { + "date": "2022-08-15", + "commits": 5 + }, + { + "date": "2022-08-16", + "commits": 10 + }, + { + "date": "2022-08-17", + "commits": 7 + }, + { + "date": "2022-08-18", + "commits": 8 + }, + { + "date": "2022-08-19", + "commits": 12 + }, + { + "date": "2022-08-20", + "commits": 5 + }, + { + "date": "2022-08-21", + "commits": 0 + }, + { + "date": "2022-08-22", + "commits": 14 + }, + { + "date": "2022-08-23", + "commits": 8 + }, + { + "date": "2022-08-24", + "commits": 10 + }, + { + "date": "2022-08-25", + "commits": 6 + }, + { + "date": "2022-08-26", + "commits": 7 + }, + { + "date": "2022-08-27", + "commits": 0 + }, + { + "date": "2022-08-28", + "commits": 7 + }, + { + "date": "2022-08-29", + "commits": 12 + }, + { + "date": "2022-08-30", + "commits": 4 + }, + { + "date": "2022-08-31", + "commits": 3 + }, + { + "date": "2022-09-01", + "commits": 3 + }, + { + "date": "2022-09-02", + "commits": 11 + }, + { + "date": "2022-09-03", + "commits": 0 + }, + { + "date": "2022-09-04", + "commits": 1 + }, + { + "date": "2022-09-05", + "commits": 7 + }, + { + "date": "2022-09-06", + "commits": 7 + }, + { + "date": "2022-09-07", + "commits": 4 + }, + { + "date": "2022-09-08", + "commits": 8 + }, + { + "date": "2022-09-09", + "commits": 3 + }, + { + "date": "2022-09-10", + "commits": 1 + }, + { + "date": "2022-09-11", + "commits": 2 + }, + { + "date": "2022-09-12", + "commits": 9 + }, + { + "date": "2022-09-13", + "commits": 7 + }, + { + "date": "2022-09-14", + "commits": 5 + }, + { + "date": "2022-09-15", + "commits": 8 + }, + { + "date": "2022-09-16", + "commits": 6 + }, + { + "date": "2022-09-17", + "commits": 6 + }, + { + "date": "2022-09-18", + "commits": 4 + }, + { + "date": "2022-09-19", + "commits": 7 + }, + { + "date": "2022-09-20", + "commits": 6 + }, + { + "date": "2022-09-21", + "commits": 5 + }, + { + "date": "2022-09-22", + "commits": 5 + }, + { + "date": "2022-09-23", + "commits": 6 + }, + { + "date": "2022-09-24", + "commits": 2 + }, + { + "date": "2022-09-25", + "commits": 3 + }, + { + "date": "2022-09-26", + "commits": 9 + }, + { + "date": "2022-09-27", + "commits": 14 + }, + { + "date": "2022-09-28", + "commits": 5 + }, + { + "date": "2022-09-29", + "commits": 8 + }, + { + "date": "2022-09-30", + "commits": 16 + }, + { + "date": "2022-10-01", + "commits": 0 + }, + { + "date": "2022-10-02", + "commits": 0 + }, + { + "date": "2022-10-03", + "commits": 8 + }, + { + "date": "2022-10-04", + "commits": 8 + }, + { + "date": "2022-10-05", + "commits": 9 + }, + { + "date": "2022-10-06", + "commits": 7 + }, + { + "date": "2022-10-07", + "commits": 9 + }, + { + "date": "2022-10-08", + "commits": 2 + }, + { + "date": "2022-10-09", + "commits": 4 + }, + { + "date": "2022-10-10", + "commits": 2 + }, + { + "date": "2022-10-11", + "commits": 5 + }, + { + "date": "2022-10-12", + "commits": 7 + }, + { + "date": "2022-10-13", + "commits": 4 + }, + { + "date": "2022-10-14", + "commits": 4 + }, + { + "date": "2022-10-15", + "commits": 3 + }, + { + "date": "2022-10-16", + "commits": 0 + }, + { + "date": "2022-10-17", + "commits": 9 + }, + { + "date": "2022-10-18", + "commits": 10 + }, + { + "date": "2022-10-19", + "commits": 8 + }, + { + "date": "2022-10-20", + "commits": 9 + }, + { + "date": "2022-10-21", + "commits": 7 + }, + { + "date": "2022-10-22", + "commits": 0 + }, + { + "date": "2022-10-23", + "commits": 1 + }, + { + "date": "2022-10-24", + "commits": 9 + }, + { + "date": "2022-10-25", + "commits": 6 + }, + { + "date": "2022-10-26", + "commits": 7 + }, + { + "date": "2022-10-27", + "commits": 10 + }, + { + "date": "2022-10-28", + "commits": 5 + }, + { + "date": "2022-10-29", + "commits": 3 + }, + { + "date": "2022-10-30", + "commits": 7 + }, + { + "date": "2022-10-31", + "commits": 6 + }, + { + "date": "2022-11-01", + "commits": 8 + }, + { + "date": "2022-11-02", + "commits": 7 + }, + { + "date": "2022-11-03", + "commits": 12 + }, + { + "date": "2022-11-04", + "commits": 8 + }, + { + "date": "2022-11-05", + "commits": 4 + }, + { + "date": "2022-11-06", + "commits": 0 + }, + { + "date": "2022-11-07", + "commits": 7 + }, + { + "date": "2022-11-08", + "commits": 7 + }, + { + "date": "2022-11-09", + "commits": 2 + }, + { + "date": "2022-11-10", + "commits": 12 + }, + { + "date": "2022-11-11", + "commits": 10 + }, + { + "date": "2022-11-12", + "commits": 0 + }, + { + "date": "2022-11-13", + "commits": 4 + }, + { + "date": "2022-11-14", + "commits": 7 + }, + { + "date": "2022-11-15", + "commits": 9 + }, + { + "date": "2022-11-16", + "commits": 9 + }, + { + "date": "2022-11-17", + "commits": 3 + }, + { + "date": "2022-11-18", + "commits": 7 + }, + { + "date": "2022-11-19", + "commits": 6 + }, + { + "date": "2022-11-20", + "commits": 0 + }, + { + "date": "2022-11-21", + "commits": 4 + }, + { + "date": "2022-11-22", + "commits": 3 + }, + { + "date": "2022-11-23", + "commits": 4 + }, + { + "date": "2022-11-24", + "commits": 9 + }, + { + "date": "2022-11-25", + "commits": 13 + }, + { + "date": "2022-11-26", + "commits": 3 + }, + { + "date": "2022-11-27", + "commits": 2 + }, + { + "date": "2022-11-28", + "commits": 14 + }, + { + "date": "2022-11-29", + "commits": 6 + }, + { + "date": "2022-11-30", + "commits": 5 + }, + { + "date": "2022-12-01", + "commits": 9 + }, + { + "date": "2022-12-02", + "commits": 9 + }, + { + "date": "2022-12-03", + "commits": 0 + }, + { + "date": "2022-12-04", + "commits": 0 + }, + { + "date": "2022-12-05", + "commits": 8 + }, + { + "date": "2022-12-06", + "commits": 8 + }, + { + "date": "2022-12-07", + "commits": 4 + }, + { + "date": "2022-12-08", + "commits": 7 + }, + { + "date": "2022-12-09", + "commits": 6 + }, + { + "date": "2022-12-10", + "commits": 3 + }, + { + "date": "2022-12-11", + "commits": 1 + }, + { + "date": "2022-12-12", + "commits": 7 + }, + { + "date": "2022-12-13", + "commits": 6 + }, + { + "date": "2022-12-14", + "commits": 11 + }, + { + "date": "2022-12-15", + "commits": 12 + }, + { + "date": "2022-12-16", + "commits": 6 + }, + { + "date": "2022-12-17", + "commits": 4 + }, + { + "date": "2022-12-18", + "commits": 0 + }, + { + "date": "2022-12-19", + "commits": 8 + }, + { + "date": "2022-12-20", + "commits": 10 + }, + { + "date": "2022-12-21", + "commits": 12 + }, + { + "date": "2022-12-22", + "commits": 6 + }, + { + "date": "2022-12-23", + "commits": 7 + }, + { + "date": "2022-12-24", + "commits": 2 + }, + { + "date": "2022-12-25", + "commits": 0 + }, + { + "date": "2022-12-26", + "commits": 8 + }, + { + "date": "2022-12-27", + "commits": 5 + }, + { + "date": "2022-12-28", + "commits": 9 + }, + { + "date": "2022-12-29", + "commits": 4 + }, + { + "date": "2022-12-30", + "commits": 2 + }, + { + "date": "2022-12-31", + "commits": 2 + }, + { + "date": "2023-01-01", + "commits": 2 + }, + { + "date": "2023-01-02", + "commits": 6 + }, + { + "date": "2023-01-03", + "commits": 10 + }, + { + "date": "2023-01-04", + "commits": 7 + }, + { + "date": "2023-01-05", + "commits": 6 + }, + { + "date": "2023-01-06", + "commits": 9 + }, + { + "date": "2023-01-07", + "commits": 0 + }, + { + "date": "2023-01-08", + "commits": 0 + }, + { + "date": "2023-01-09", + "commits": 7 + }, + { + "date": "2023-01-10", + "commits": 10 + }, + { + "date": "2023-01-11", + "commits": 7 + }, + { + "date": "2023-01-12", + "commits": 8 + }, + { + "date": "2023-01-13", + "commits": 6 + }, + { + "date": "2023-01-14", + "commits": 2 + }, + { + "date": "2023-01-15", + "commits": 6 + }, + { + "date": "2023-01-16", + "commits": 5 + }, + { + "date": "2023-01-17", + "commits": 15 + }, + { + "date": "2023-01-18", + "commits": 6 + }, + { + "date": "2023-01-19", + "commits": 8 + }, + { + "date": "2023-01-20", + "commits": 8 + }, + { + "date": "2023-01-21", + "commits": 5 + }, + { + "date": "2023-01-22", + "commits": 0 + }, + { + "date": "2023-01-23", + "commits": 1 + }, + { + "date": "2023-01-24", + "commits": 9 + }, + { + "date": "2023-01-25", + "commits": 10 + }, + { + "date": "2023-01-26", + "commits": 9 + }, + { + "date": "2023-01-27", + "commits": 15 + }, + { + "date": "2023-01-28", + "commits": 2 + }, + { + "date": "2023-01-29", + "commits": 2 + }, + { + "date": "2023-01-30", + "commits": 10 + }, + { + "date": "2023-01-31", + "commits": 9 + }, + { + "date": "2023-02-01", + "commits": 12 + }, + { + "date": "2023-02-02", + "commits": 4 + }, + { + "date": "2023-02-03", + "commits": 6 + }, + { + "date": "2023-02-04", + "commits": 0 + }, + { + "date": "2023-02-05", + "commits": 4 + }, + { + "date": "2023-02-06", + "commits": 6 + }, + { + "date": "2023-02-07", + "commits": 10 + }, + { + "date": "2023-02-08", + "commits": 14 + }, + { + "date": "2023-02-09", + "commits": 7 + }, + { + "date": "2023-02-10", + "commits": 7 + }, + { + "date": "2023-02-11", + "commits": 0 + }, + { + "date": "2023-02-12", + "commits": 0 + }, + { + "date": "2023-02-13", + "commits": 6 + }, + { + "date": "2023-02-14", + "commits": 9 + }, + { + "date": "2023-02-15", + "commits": 8 + }, + { + "date": "2023-02-16", + "commits": 8 + }, + { + "date": "2023-02-17", + "commits": 7 + }, + { + "date": "2023-02-18", + "commits": 4 + }, + { + "date": "2023-02-19", + "commits": 3 + }, + { + "date": "2023-02-20", + "commits": 7 + }, + { + "date": "2023-02-21", + "commits": 9 + }, + { + "date": "2023-02-22", + "commits": 7 + }, + { + "date": "2023-02-23", + "commits": 4 + }, + { + "date": "2023-02-24", + "commits": 12 + }, + { + "date": "2023-02-25", + "commits": 3 + }, + { + "date": "2023-02-26", + "commits": 0 + }, + { + "date": "2023-02-27", + "commits": 11 + }, + { + "date": "2023-02-28", + "commits": 9 + }, + { + "date": "2023-03-01", + "commits": 3 + }, + { + "date": "2023-03-02", + "commits": 12 + }, + { + "date": "2023-03-03", + "commits": 9 + }, + { + "date": "2023-03-04", + "commits": 4 + }, + { + "date": "2023-03-05", + "commits": 2 + }, + { + "date": "2023-03-06", + "commits": 7 + }, + { + "date": "2023-03-07", + "commits": 3 + }, + { + "date": "2023-03-08", + "commits": 10 + }, + { + "date": "2023-03-09", + "commits": 8 + }, + { + "date": "2023-03-10", + "commits": 7 + }, + { + "date": "2023-03-11", + "commits": 3 + }, + { + "date": "2023-03-12", + "commits": 2 + }, + { + "date": "2023-03-13", + "commits": 10 + }, + { + "date": "2023-03-14", + "commits": 6 + }, + { + "date": "2023-03-15", + "commits": 7 + }, + { + "date": "2023-03-16", + "commits": 1 + }, + { + "date": "2023-03-17", + "commits": 6 + }, + { + "date": "2023-03-18", + "commits": 4 + }, + { + "date": "2023-03-19", + "commits": 6 + }, + { + "date": "2023-03-20", + "commits": 6 + }, + { + "date": "2023-03-21", + "commits": 7 + }, + { + "date": "2023-03-22", + "commits": 12 + }, + { + "date": "2023-03-23", + "commits": 7 + }, + { + "date": "2023-03-24", + "commits": 10 + }, + { + "date": "2023-03-25", + "commits": 7 + }, + { + "date": "2023-03-26", + "commits": 2 + }, + { + "date": "2023-03-27", + "commits": 11 + }, + { + "date": "2023-03-28", + "commits": 5 + }, + { + "date": "2023-03-29", + "commits": 8 + }, + { + "date": "2023-03-30", + "commits": 7 + }, + { + "date": "2023-03-31", + "commits": 8 + }, + { + "date": "2023-04-01", + "commits": 5 + }, + { + "date": "2023-04-02", + "commits": 9 + }, + { + "date": "2023-04-03", + "commits": 6 + }, + { + "date": "2023-04-04", + "commits": 6 + }, + { + "date": "2023-04-05", + "commits": 9 + }, + { + "date": "2023-04-06", + "commits": 4 + }, + { + "date": "2023-04-07", + "commits": 9 + }, + { + "date": "2023-04-08", + "commits": 3 + }, + { + "date": "2023-04-09", + "commits": 1 + }, + { + "date": "2023-04-10", + "commits": 9 + }, + { + "date": "2023-04-11", + "commits": 3 + }, + { + "date": "2023-04-12", + "commits": 10 + }, + { + "date": "2023-04-13", + "commits": 3 + }, + { + "date": "2023-04-14", + "commits": 5 + }, + { + "date": "2023-04-15", + "commits": 0 + }, + { + "date": "2023-04-16", + "commits": 0 + }, + { + "date": "2023-04-17", + "commits": 10 + }, + { + "date": "2023-04-18", + "commits": 8 + }, + { + "date": "2023-04-19", + "commits": 6 + }, + { + "date": "2023-04-20", + "commits": 9 + }, + { + "date": "2023-04-21", + "commits": 12 + }, + { + "date": "2023-04-22", + "commits": 2 + }, + { + "date": "2023-04-23", + "commits": 3 + }, + { + "date": "2023-04-24", + "commits": 11 + }, + { + "date": "2023-04-25", + "commits": 8 + }, + { + "date": "2023-04-26", + "commits": 4 + }, + { + "date": "2023-04-27", + "commits": 15 + }, + { + "date": "2023-04-28", + "commits": 14 + }, + { + "date": "2023-04-29", + "commits": 0 + }, + { + "date": "2023-04-30", + "commits": 1 + }, + { + "date": "2023-05-01", + "commits": 9 + }, + { + "date": "2023-05-02", + "commits": 10 + }, + { + "date": "2023-05-03", + "commits": 10 + }, + { + "date": "2023-05-04", + "commits": 7 + }, + { + "date": "2023-05-05", + "commits": 4 + }, + { + "date": "2023-05-06", + "commits": 2 + }, + { + "date": "2023-05-07", + "commits": 5 + }, + { + "date": "2023-05-08", + "commits": 4 + }, + { + "date": "2023-05-09", + "commits": 4 + }, + { + "date": "2023-05-10", + "commits": 7 + }, + { + "date": "2023-05-11", + "commits": 2 + }, + { + "date": "2023-05-12", + "commits": 7 + }, + { + "date": "2023-05-13", + "commits": 0 + }, + { + "date": "2023-05-14", + "commits": 3 + }, + { + "date": "2023-05-15", + "commits": 5 + }, + { + "date": "2023-05-16", + "commits": 5 + }, + { + "date": "2023-05-17", + "commits": 6 + }, + { + "date": "2023-05-18", + "commits": 7 + }, + { + "date": "2023-05-19", + "commits": 6 + }, + { + "date": "2023-05-20", + "commits": 2 + }, + { + "date": "2023-05-21", + "commits": 4 + }, + { + "date": "2023-05-22", + "commits": 11 + }, + { + "date": "2023-05-23", + "commits": 13 + }, + { + "date": "2023-05-24", + "commits": 5 + }, + { + "date": "2023-05-25", + "commits": 6 + }, + { + "date": "2023-05-26", + "commits": 0 + }, + { + "date": "2023-05-27", + "commits": 7 + }, + { + "date": "2023-05-28", + "commits": 0 + }, + { + "date": "2023-05-29", + "commits": 7 + }, + { + "date": "2023-05-30", + "commits": 9 + }, + { + "date": "2023-05-31", + "commits": 3 + }, + { + "date": "2023-06-01", + "commits": 9 + }, + { + "date": "2023-06-02", + "commits": 7 + }, + { + "date": "2023-06-03", + "commits": 0 + }, + { + "date": "2023-06-04", + "commits": 2 + }, + { + "date": "2023-06-05", + "commits": 11 + }, + { + "date": "2023-06-06", + "commits": 2 + }, + { + "date": "2023-06-07", + "commits": 10 + }, + { + "date": "2023-06-08", + "commits": 8 + }, + { + "date": "2023-06-09", + "commits": 9 + }, + { + "date": "2023-06-10", + "commits": 3 + }, + { + "date": "2023-06-11", + "commits": 5 + }, + { + "date": "2023-06-12", + "commits": 7 + }, + { + "date": "2023-06-13", + "commits": 10 + }, + { + "date": "2023-06-14", + "commits": 6 + }, + { + "date": "2023-06-15", + "commits": 10 + }, + { + "date": "2023-06-16", + "commits": 5 + }, + { + "date": "2023-06-17", + "commits": 1 + }, + { + "date": "2023-06-18", + "commits": 7 + }, + { + "date": "2023-06-19", + "commits": 9 + }, + { + "date": "2023-06-20", + "commits": 7 + }, + { + "date": "2023-06-21", + "commits": 4 + }, + { + "date": "2023-06-22", + "commits": 5 + }, + { + "date": "2023-06-23", + "commits": 8 + }, + { + "date": "2023-06-24", + "commits": 4 + }, + { + "date": "2023-06-25", + "commits": 3 + }, + { + "date": "2023-06-26", + "commits": 9 + }, + { + "date": "2023-06-27", + "commits": 7 + }, + { + "date": "2023-06-28", + "commits": 12 + }, + { + "date": "2023-06-29", + "commits": 6 + }, + { + "date": "2023-06-30", + "commits": 6 + }, + { + "date": "2023-07-01", + "commits": 4 + }, + { + "date": "2023-07-02", + "commits": 2 + }, + { + "date": "2023-07-03", + "commits": 7 + }, + { + "date": "2023-07-04", + "commits": 6 + }, + { + "date": "2023-07-05", + "commits": 7 + }, + { + "date": "2023-07-06", + "commits": 9 + }, + { + "date": "2023-07-07", + "commits": 9 + }, + { + "date": "2023-07-08", + "commits": 0 + }, + { + "date": "2023-07-09", + "commits": 3 + }, + { + "date": "2023-07-10", + "commits": 8 + }, + { + "date": "2023-07-11", + "commits": 4 + }, + { + "date": "2023-07-12", + "commits": 10 + }, + { + "date": "2023-07-13", + "commits": 7 + }, + { + "date": "2023-07-14", + "commits": 6 + }, + { + "date": "2023-07-15", + "commits": 4 + }, + { + "date": "2023-07-16", + "commits": 5 + }, + { + "date": "2023-07-17", + "commits": 5 + }, + { + "date": "2023-07-18", + "commits": 9 + }, + { + "date": "2023-07-19", + "commits": 5 + }, + { + "date": "2023-07-20", + "commits": 14 + }, + { + "date": "2023-07-21", + "commits": 6 + }, + { + "date": "2023-07-22", + "commits": 5 + }, + { + "date": "2023-07-23", + "commits": 0 + }, + { + "date": "2023-07-24", + "commits": 10 + }, + { + "date": "2023-07-25", + "commits": 14 + }, + { + "date": "2023-07-26", + "commits": 0 + }, + { + "date": "2023-07-27", + "commits": 6 + }, + { + "date": "2023-07-28", + "commits": 9 + }, + { + "date": "2023-07-29", + "commits": 1 + }, + { + "date": "2023-07-30", + "commits": 0 + }, + { + "date": "2023-07-31", + "commits": 14 + }, + { + "date": "2023-08-01", + "commits": 8 + }, + { + "date": "2023-08-02", + "commits": 3 + }, + { + "date": "2023-08-03", + "commits": 10 + }, + { + "date": "2023-08-04", + "commits": 2 + }, + { + "date": "2023-08-05", + "commits": 5 + }, + { + "date": "2023-08-06", + "commits": 0 + }, + { + "date": "2023-08-07", + "commits": 8 + }, + { + "date": "2023-08-08", + "commits": 11 + }, + { + "date": "2023-08-09", + "commits": 8 + }, + { + "date": "2023-08-10", + "commits": 3 + }, + { + "date": "2023-08-11", + "commits": 2 + }, + { + "date": "2023-08-12", + "commits": 5 + }, + { + "date": "2023-08-13", + "commits": 4 + }, + { + "date": "2023-08-14", + "commits": 5 + }, + { + "date": "2023-08-15", + "commits": 10 + }, + { + "date": "2023-08-16", + "commits": 9 + }, + { + "date": "2023-08-17", + "commits": 9 + }, + { + "date": "2023-08-18", + "commits": 1 + }, + { + "date": "2023-08-19", + "commits": 1 + }, + { + "date": "2023-08-20", + "commits": 4 + }, + { + "date": "2023-08-21", + "commits": 10 + }, + { + "date": "2023-08-22", + "commits": 10 + }, + { + "date": "2023-08-23", + "commits": 0 + }, + { + "date": "2023-08-24", + "commits": 8 + }, + { + "date": "2023-08-25", + "commits": 9 + }, + { + "date": "2023-08-26", + "commits": 9 + }, + { + "date": "2023-08-27", + "commits": 0 + }, + { + "date": "2023-08-28", + "commits": 7 + }, + { + "date": "2023-08-29", + "commits": 8 + }, + { + "date": "2023-08-30", + "commits": 10 + }, + { + "date": "2023-08-31", + "commits": 6 + }, + { + "date": "2023-09-01", + "commits": 11 + }, + { + "date": "2023-09-02", + "commits": 0 + }, + { + "date": "2023-09-03", + "commits": 2 + }, + { + "date": "2023-09-04", + "commits": 6 + }, + { + "date": "2023-09-05", + "commits": 8 + }, + { + "date": "2023-09-06", + "commits": 5 + }, + { + "date": "2023-09-07", + "commits": 3 + }, + { + "date": "2023-09-08", + "commits": 11 + }, + { + "date": "2023-09-09", + "commits": 2 + }, + { + "date": "2023-09-10", + "commits": 0 + }, + { + "date": "2023-09-11", + "commits": 8 + }, + { + "date": "2023-09-12", + "commits": 10 + }, + { + "date": "2023-09-13", + "commits": 5 + }, + { + "date": "2023-09-14", + "commits": 7 + }, + { + "date": "2023-09-15", + "commits": 9 + }, + { + "date": "2023-09-16", + "commits": 3 + }, + { + "date": "2023-09-17", + "commits": 0 + }, + { + "date": "2023-09-18", + "commits": 1 + }, + { + "date": "2023-09-19", + "commits": 11 + }, + { + "date": "2023-09-20", + "commits": 8 + }, + { + "date": "2023-09-21", + "commits": 8 + }, + { + "date": "2023-09-22", + "commits": 7 + }, + { + "date": "2023-09-23", + "commits": 2 + }, + { + "date": "2023-09-24", + "commits": 0 + }, + { + "date": "2023-09-25", + "commits": 4 + }, + { + "date": "2023-09-26", + "commits": 5 + }, + { + "date": "2023-09-27", + "commits": 6 + }, + { + "date": "2023-09-28", + "commits": 6 + }, + { + "date": "2023-09-29", + "commits": 4 + }, + { + "date": "2023-09-30", + "commits": 3 + }, + { + "date": "2023-10-01", + "commits": 0 + }, + { + "date": "2023-10-02", + "commits": 9 + }, + { + "date": "2023-10-03", + "commits": 4 + }, + { + "date": "2023-10-04", + "commits": 9 + }, + { + "date": "2023-10-05", + "commits": 12 + }, + { + "date": "2023-10-06", + "commits": 8 + }, + { + "date": "2023-10-07", + "commits": 0 + }, + { + "date": "2023-10-08", + "commits": 2 + }, + { + "date": "2023-10-09", + "commits": 8 + }, + { + "date": "2023-10-10", + "commits": 2 + }, + { + "date": "2023-10-11", + "commits": 6 + }, + { + "date": "2023-10-12", + "commits": 8 + }, + { + "date": "2023-10-13", + "commits": 6 + }, + { + "date": "2023-10-14", + "commits": 2 + }, + { + "date": "2023-10-15", + "commits": 4 + }, + { + "date": "2023-10-16", + "commits": 10 + }, + { + "date": "2023-10-17", + "commits": 10 + }, + { + "date": "2023-10-18", + "commits": 9 + }, + { + "date": "2023-10-19", + "commits": 7 + }, + { + "date": "2023-10-20", + "commits": 7 + }, + { + "date": "2023-10-21", + "commits": 1 + }, + { + "date": "2023-10-22", + "commits": 1 + }, + { + "date": "2023-10-23", + "commits": 7 + }, + { + "date": "2023-10-24", + "commits": 2 + }, + { + "date": "2023-10-25", + "commits": 7 + }, + { + "date": "2023-10-26", + "commits": 7 + }, + { + "date": "2023-10-27", + "commits": 5 + }, + { + "date": "2023-10-28", + "commits": 1 + }, + { + "date": "2023-10-29", + "commits": 3 + }, + { + "date": "2023-10-30", + "commits": 7 + }, + { + "date": "2023-10-31", + "commits": 14 + }, + { + "date": "2023-11-01", + "commits": 0 + }, + { + "date": "2023-11-02", + "commits": 7 + }, + { + "date": "2023-11-03", + "commits": 2 + }, + { + "date": "2023-11-04", + "commits": 4 + }, + { + "date": "2023-11-05", + "commits": 9 + }, + { + "date": "2023-11-06", + "commits": 0 + }, + { + "date": "2023-11-07", + "commits": 8 + }, + { + "date": "2023-11-08", + "commits": 9 + }, + { + "date": "2023-11-09", + "commits": 7 + }, + { + "date": "2023-11-10", + "commits": 9 + }, + { + "date": "2023-11-11", + "commits": 0 + }, + { + "date": "2023-11-12", + "commits": 4 + }, + { + "date": "2023-11-13", + "commits": 9 + }, + { + "date": "2023-11-14", + "commits": 8 + }, + { + "date": "2023-11-15", + "commits": 6 + }, + { + "date": "2023-11-16", + "commits": 9 + }, + { + "date": "2023-11-17", + "commits": 6 + }, + { + "date": "2023-11-18", + "commits": 2 + }, + { + "date": "2023-11-19", + "commits": 0 + }, + { + "date": "2023-11-20", + "commits": 1 + }, + { + "date": "2023-11-21", + "commits": 7 + }, + { + "date": "2023-11-22", + "commits": 8 + }, + { + "date": "2023-11-23", + "commits": 10 + }, + { + "date": "2023-11-24", + "commits": 5 + }, + { + "date": "2023-11-25", + "commits": 1 + }, + { + "date": "2023-11-26", + "commits": 3 + }, + { + "date": "2023-11-27", + "commits": 8 + }, + { + "date": "2023-11-28", + "commits": 11 + }, + { + "date": "2023-11-29", + "commits": 13 + }, + { + "date": "2023-11-30", + "commits": 5 + }, + { + "date": "2023-12-01", + "commits": 2 + }, + { + "date": "2023-12-02", + "commits": 4 + } + ] + }, + "semantic_types": { + "date": "Date", + "commits": "Quantity" + }, + "chart_spec": { + "chartType": "Calendar Heatmap", + "encodings": { + "x": { + "field": "date" + }, + "color": { + "field": "commits" + } + }, + "canvasSize": { + "width": 480, + "height": 320 + } + } + } +} \ No newline at end of file diff --git a/recursive/echarts-testing/test_cases/cal_quarter.json b/recursive/echarts-testing/test_cases/cal_quarter.json new file mode 100644 index 00000000..6dc70daa --- /dev/null +++ b/recursive/echarts-testing/test_cases/cal_quarter.json @@ -0,0 +1,509 @@ +{ + "test_id": "cal_quarter", + "description": "Calendar heatmap, ~120 daily values (one quarter)", + "input": { + "data": { + "values": [ + { + "date": "2023-01-01", + "commits": 1 + }, + { + "date": "2023-01-02", + "commits": 9 + }, + { + "date": "2023-01-03", + "commits": 7 + }, + { + "date": "2023-01-04", + "commits": 7 + }, + { + "date": "2023-01-05", + "commits": 5 + }, + { + "date": "2023-01-06", + "commits": 7 + }, + { + "date": "2023-01-07", + "commits": 5 + }, + { + "date": "2023-01-08", + "commits": 3 + }, + { + "date": "2023-01-09", + "commits": 11 + }, + { + "date": "2023-01-10", + "commits": 8 + }, + { + "date": "2023-01-11", + "commits": 9 + }, + { + "date": "2023-01-12", + "commits": 8 + }, + { + "date": "2023-01-13", + "commits": 3 + }, + { + "date": "2023-01-14", + "commits": 4 + }, + { + "date": "2023-01-15", + "commits": 3 + }, + { + "date": "2023-01-16", + "commits": 9 + }, + { + "date": "2023-01-17", + "commits": 2 + }, + { + "date": "2023-01-18", + "commits": 2 + }, + { + "date": "2023-01-19", + "commits": 5 + }, + { + "date": "2023-01-20", + "commits": 6 + }, + { + "date": "2023-01-21", + "commits": 2 + }, + { + "date": "2023-01-22", + "commits": 1 + }, + { + "date": "2023-01-23", + "commits": 9 + }, + { + "date": "2023-01-24", + "commits": 6 + }, + { + "date": "2023-01-25", + "commits": 8 + }, + { + "date": "2023-01-26", + "commits": 9 + }, + { + "date": "2023-01-27", + "commits": 6 + }, + { + "date": "2023-01-28", + "commits": 7 + }, + { + "date": "2023-01-29", + "commits": 3 + }, + { + "date": "2023-01-30", + "commits": 11 + }, + { + "date": "2023-01-31", + "commits": 6 + }, + { + "date": "2023-02-01", + "commits": 5 + }, + { + "date": "2023-02-02", + "commits": 6 + }, + { + "date": "2023-02-03", + "commits": 7 + }, + { + "date": "2023-02-04", + "commits": 3 + }, + { + "date": "2023-02-05", + "commits": 2 + }, + { + "date": "2023-02-06", + "commits": 6 + }, + { + "date": "2023-02-07", + "commits": 5 + }, + { + "date": "2023-02-08", + "commits": 6 + }, + { + "date": "2023-02-09", + "commits": 11 + }, + { + "date": "2023-02-10", + "commits": 5 + }, + { + "date": "2023-02-11", + "commits": 2 + }, + { + "date": "2023-02-12", + "commits": 3 + }, + { + "date": "2023-02-13", + "commits": 3 + }, + { + "date": "2023-02-14", + "commits": 8 + }, + { + "date": "2023-02-15", + "commits": 11 + }, + { + "date": "2023-02-16", + "commits": 1 + }, + { + "date": "2023-02-17", + "commits": 7 + }, + { + "date": "2023-02-18", + "commits": 1 + }, + { + "date": "2023-02-19", + "commits": 0 + }, + { + "date": "2023-02-20", + "commits": 9 + }, + { + "date": "2023-02-21", + "commits": 7 + }, + { + "date": "2023-02-22", + "commits": 3 + }, + { + "date": "2023-02-23", + "commits": 10 + }, + { + "date": "2023-02-24", + "commits": 10 + }, + { + "date": "2023-02-25", + "commits": 4 + }, + { + "date": "2023-02-26", + "commits": 6 + }, + { + "date": "2023-02-27", + "commits": 9 + }, + { + "date": "2023-02-28", + "commits": 8 + }, + { + "date": "2023-03-01", + "commits": 4 + }, + { + "date": "2023-03-02", + "commits": 9 + }, + { + "date": "2023-03-03", + "commits": 6 + }, + { + "date": "2023-03-04", + "commits": 0 + }, + { + "date": "2023-03-05", + "commits": 0 + }, + { + "date": "2023-03-06", + "commits": 5 + }, + { + "date": "2023-03-07", + "commits": 6 + }, + { + "date": "2023-03-08", + "commits": 11 + }, + { + "date": "2023-03-09", + "commits": 1 + }, + { + "date": "2023-03-10", + "commits": 3 + }, + { + "date": "2023-03-11", + "commits": 2 + }, + { + "date": "2023-03-12", + "commits": 6 + }, + { + "date": "2023-03-13", + "commits": 9 + }, + { + "date": "2023-03-14", + "commits": 2 + }, + { + "date": "2023-03-15", + "commits": 0 + }, + { + "date": "2023-03-16", + "commits": 9 + }, + { + "date": "2023-03-17", + "commits": 5 + }, + { + "date": "2023-03-18", + "commits": 0 + }, + { + "date": "2023-03-19", + "commits": 4 + }, + { + "date": "2023-03-20", + "commits": 11 + }, + { + "date": "2023-03-21", + "commits": 8 + }, + { + "date": "2023-03-22", + "commits": 8 + }, + { + "date": "2023-03-23", + "commits": 9 + }, + { + "date": "2023-03-24", + "commits": 12 + }, + { + "date": "2023-03-25", + "commits": 3 + }, + { + "date": "2023-03-26", + "commits": 3 + }, + { + "date": "2023-03-27", + "commits": 9 + }, + { + "date": "2023-03-28", + "commits": 3 + }, + { + "date": "2023-03-29", + "commits": 11 + }, + { + "date": "2023-03-30", + "commits": 10 + }, + { + "date": "2023-03-31", + "commits": 9 + }, + { + "date": "2023-04-01", + "commits": 0 + }, + { + "date": "2023-04-02", + "commits": 0 + }, + { + "date": "2023-04-03", + "commits": 10 + }, + { + "date": "2023-04-04", + "commits": 2 + }, + { + "date": "2023-04-05", + "commits": 7 + }, + { + "date": "2023-04-06", + "commits": 11 + }, + { + "date": "2023-04-07", + "commits": 4 + }, + { + "date": "2023-04-08", + "commits": 6 + }, + { + "date": "2023-04-09", + "commits": 3 + }, + { + "date": "2023-04-10", + "commits": 7 + }, + { + "date": "2023-04-11", + "commits": 8 + }, + { + "date": "2023-04-12", + "commits": 9 + }, + { + "date": "2023-04-13", + "commits": 8 + }, + { + "date": "2023-04-14", + "commits": 11 + }, + { + "date": "2023-04-15", + "commits": 0 + }, + { + "date": "2023-04-16", + "commits": 0 + }, + { + "date": "2023-04-17", + "commits": 11 + }, + { + "date": "2023-04-18", + "commits": 8 + }, + { + "date": "2023-04-19", + "commits": 5 + }, + { + "date": "2023-04-20", + "commits": 10 + }, + { + "date": "2023-04-21", + "commits": 12 + }, + { + "date": "2023-04-22", + "commits": 0 + }, + { + "date": "2023-04-23", + "commits": 0 + }, + { + "date": "2023-04-24", + "commits": 7 + }, + { + "date": "2023-04-25", + "commits": 7 + }, + { + "date": "2023-04-26", + "commits": 7 + }, + { + "date": "2023-04-27", + "commits": 12 + }, + { + "date": "2023-04-28", + "commits": 4 + }, + { + "date": "2023-04-29", + "commits": 5 + }, + { + "date": "2023-04-30", + "commits": 0 + } + ] + }, + "semantic_types": { + "date": "Date", + "commits": "Quantity" + }, + "chart_spec": { + "chartType": "Calendar Heatmap", + "encodings": { + "x": { + "field": "date" + }, + "color": { + "field": "commits" + } + }, + "canvasSize": { + "width": 480, + "height": 320 + } + } + } +} \ No newline at end of file diff --git a/recursive/echarts-testing/test_cases/cal_year.json b/recursive/echarts-testing/test_cases/cal_year.json new file mode 100644 index 00000000..0dc4069d --- /dev/null +++ b/recursive/echarts-testing/test_cases/cal_year.json @@ -0,0 +1,1489 @@ +{ + "test_id": "cal_year", + "description": "Calendar heatmap, full year (365 daily values)", + "input": { + "data": { + "values": [ + { + "date": "2023-01-01", + "commits": 0 + }, + { + "date": "2023-01-02", + "commits": 9 + }, + { + "date": "2023-01-03", + "commits": 11 + }, + { + "date": "2023-01-04", + "commits": 10 + }, + { + "date": "2023-01-05", + "commits": 9 + }, + { + "date": "2023-01-06", + "commits": 8 + }, + { + "date": "2023-01-07", + "commits": 2 + }, + { + "date": "2023-01-08", + "commits": 3 + }, + { + "date": "2023-01-09", + "commits": 7 + }, + { + "date": "2023-01-10", + "commits": 8 + }, + { + "date": "2023-01-11", + "commits": 9 + }, + { + "date": "2023-01-12", + "commits": 8 + }, + { + "date": "2023-01-13", + "commits": 10 + }, + { + "date": "2023-01-14", + "commits": 3 + }, + { + "date": "2023-01-15", + "commits": 8 + }, + { + "date": "2023-01-16", + "commits": 8 + }, + { + "date": "2023-01-17", + "commits": 6 + }, + { + "date": "2023-01-18", + "commits": 6 + }, + { + "date": "2023-01-19", + "commits": 7 + }, + { + "date": "2023-01-20", + "commits": 10 + }, + { + "date": "2023-01-21", + "commits": 0 + }, + { + "date": "2023-01-22", + "commits": 3 + }, + { + "date": "2023-01-23", + "commits": 13 + }, + { + "date": "2023-01-24", + "commits": 0 + }, + { + "date": "2023-01-25", + "commits": 4 + }, + { + "date": "2023-01-26", + "commits": 8 + }, + { + "date": "2023-01-27", + "commits": 9 + }, + { + "date": "2023-01-28", + "commits": 2 + }, + { + "date": "2023-01-29", + "commits": 0 + }, + { + "date": "2023-01-30", + "commits": 9 + }, + { + "date": "2023-01-31", + "commits": 8 + }, + { + "date": "2023-02-01", + "commits": 6 + }, + { + "date": "2023-02-02", + "commits": 15 + }, + { + "date": "2023-02-03", + "commits": 9 + }, + { + "date": "2023-02-04", + "commits": 0 + }, + { + "date": "2023-02-05", + "commits": 1 + }, + { + "date": "2023-02-06", + "commits": 7 + }, + { + "date": "2023-02-07", + "commits": 7 + }, + { + "date": "2023-02-08", + "commits": 0 + }, + { + "date": "2023-02-09", + "commits": 6 + }, + { + "date": "2023-02-10", + "commits": 11 + }, + { + "date": "2023-02-11", + "commits": 0 + }, + { + "date": "2023-02-12", + "commits": 1 + }, + { + "date": "2023-02-13", + "commits": 10 + }, + { + "date": "2023-02-14", + "commits": 10 + }, + { + "date": "2023-02-15", + "commits": 12 + }, + { + "date": "2023-02-16", + "commits": 2 + }, + { + "date": "2023-02-17", + "commits": 6 + }, + { + "date": "2023-02-18", + "commits": 0 + }, + { + "date": "2023-02-19", + "commits": 3 + }, + { + "date": "2023-02-20", + "commits": 11 + }, + { + "date": "2023-02-21", + "commits": 0 + }, + { + "date": "2023-02-22", + "commits": 11 + }, + { + "date": "2023-02-23", + "commits": 3 + }, + { + "date": "2023-02-24", + "commits": 10 + }, + { + "date": "2023-02-25", + "commits": 0 + }, + { + "date": "2023-02-26", + "commits": 2 + }, + { + "date": "2023-02-27", + "commits": 11 + }, + { + "date": "2023-02-28", + "commits": 7 + }, + { + "date": "2023-03-01", + "commits": 8 + }, + { + "date": "2023-03-02", + "commits": 10 + }, + { + "date": "2023-03-03", + "commits": 8 + }, + { + "date": "2023-03-04", + "commits": 1 + }, + { + "date": "2023-03-05", + "commits": 6 + }, + { + "date": "2023-03-06", + "commits": 11 + }, + { + "date": "2023-03-07", + "commits": 7 + }, + { + "date": "2023-03-08", + "commits": 16 + }, + { + "date": "2023-03-09", + "commits": 4 + }, + { + "date": "2023-03-10", + "commits": 10 + }, + { + "date": "2023-03-11", + "commits": 1 + }, + { + "date": "2023-03-12", + "commits": 2 + }, + { + "date": "2023-03-13", + "commits": 10 + }, + { + "date": "2023-03-14", + "commits": 8 + }, + { + "date": "2023-03-15", + "commits": 9 + }, + { + "date": "2023-03-16", + "commits": 3 + }, + { + "date": "2023-03-17", + "commits": 3 + }, + { + "date": "2023-03-18", + "commits": 3 + }, + { + "date": "2023-03-19", + "commits": 0 + }, + { + "date": "2023-03-20", + "commits": 4 + }, + { + "date": "2023-03-21", + "commits": 3 + }, + { + "date": "2023-03-22", + "commits": 11 + }, + { + "date": "2023-03-23", + "commits": 10 + }, + { + "date": "2023-03-24", + "commits": 12 + }, + { + "date": "2023-03-25", + "commits": 0 + }, + { + "date": "2023-03-26", + "commits": 2 + }, + { + "date": "2023-03-27", + "commits": 4 + }, + { + "date": "2023-03-28", + "commits": 10 + }, + { + "date": "2023-03-29", + "commits": 12 + }, + { + "date": "2023-03-30", + "commits": 5 + }, + { + "date": "2023-03-31", + "commits": 12 + }, + { + "date": "2023-04-01", + "commits": 4 + }, + { + "date": "2023-04-02", + "commits": 1 + }, + { + "date": "2023-04-03", + "commits": 2 + }, + { + "date": "2023-04-04", + "commits": 12 + }, + { + "date": "2023-04-05", + "commits": 7 + }, + { + "date": "2023-04-06", + "commits": 6 + }, + { + "date": "2023-04-07", + "commits": 9 + }, + { + "date": "2023-04-08", + "commits": 3 + }, + { + "date": "2023-04-09", + "commits": 6 + }, + { + "date": "2023-04-10", + "commits": 4 + }, + { + "date": "2023-04-11", + "commits": 11 + }, + { + "date": "2023-04-12", + "commits": 12 + }, + { + "date": "2023-04-13", + "commits": 12 + }, + { + "date": "2023-04-14", + "commits": 7 + }, + { + "date": "2023-04-15", + "commits": 0 + }, + { + "date": "2023-04-16", + "commits": 5 + }, + { + "date": "2023-04-17", + "commits": 8 + }, + { + "date": "2023-04-18", + "commits": 8 + }, + { + "date": "2023-04-19", + "commits": 12 + }, + { + "date": "2023-04-20", + "commits": 7 + }, + { + "date": "2023-04-21", + "commits": 1 + }, + { + "date": "2023-04-22", + "commits": 0 + }, + { + "date": "2023-04-23", + "commits": 0 + }, + { + "date": "2023-04-24", + "commits": 10 + }, + { + "date": "2023-04-25", + "commits": 8 + }, + { + "date": "2023-04-26", + "commits": 6 + }, + { + "date": "2023-04-27", + "commits": 7 + }, + { + "date": "2023-04-28", + "commits": 10 + }, + { + "date": "2023-04-29", + "commits": 2 + }, + { + "date": "2023-04-30", + "commits": 5 + }, + { + "date": "2023-05-01", + "commits": 7 + }, + { + "date": "2023-05-02", + "commits": 11 + }, + { + "date": "2023-05-03", + "commits": 12 + }, + { + "date": "2023-05-04", + "commits": 12 + }, + { + "date": "2023-05-05", + "commits": 5 + }, + { + "date": "2023-05-06", + "commits": 4 + }, + { + "date": "2023-05-07", + "commits": 0 + }, + { + "date": "2023-05-08", + "commits": 4 + }, + { + "date": "2023-05-09", + "commits": 2 + }, + { + "date": "2023-05-10", + "commits": 11 + }, + { + "date": "2023-05-11", + "commits": 4 + }, + { + "date": "2023-05-12", + "commits": 7 + }, + { + "date": "2023-05-13", + "commits": 1 + }, + { + "date": "2023-05-14", + "commits": 1 + }, + { + "date": "2023-05-15", + "commits": 6 + }, + { + "date": "2023-05-16", + "commits": 8 + }, + { + "date": "2023-05-17", + "commits": 13 + }, + { + "date": "2023-05-18", + "commits": 8 + }, + { + "date": "2023-05-19", + "commits": 9 + }, + { + "date": "2023-05-20", + "commits": 5 + }, + { + "date": "2023-05-21", + "commits": 1 + }, + { + "date": "2023-05-22", + "commits": 4 + }, + { + "date": "2023-05-23", + "commits": 6 + }, + { + "date": "2023-05-24", + "commits": 11 + }, + { + "date": "2023-05-25", + "commits": 3 + }, + { + "date": "2023-05-26", + "commits": 6 + }, + { + "date": "2023-05-27", + "commits": 5 + }, + { + "date": "2023-05-28", + "commits": 4 + }, + { + "date": "2023-05-29", + "commits": 8 + }, + { + "date": "2023-05-30", + "commits": 10 + }, + { + "date": "2023-05-31", + "commits": 8 + }, + { + "date": "2023-06-01", + "commits": 4 + }, + { + "date": "2023-06-02", + "commits": 3 + }, + { + "date": "2023-06-03", + "commits": 0 + }, + { + "date": "2023-06-04", + "commits": 4 + }, + { + "date": "2023-06-05", + "commits": 6 + }, + { + "date": "2023-06-06", + "commits": 5 + }, + { + "date": "2023-06-07", + "commits": 5 + }, + { + "date": "2023-06-08", + "commits": 3 + }, + { + "date": "2023-06-09", + "commits": 7 + }, + { + "date": "2023-06-10", + "commits": 0 + }, + { + "date": "2023-06-11", + "commits": 3 + }, + { + "date": "2023-06-12", + "commits": 0 + }, + { + "date": "2023-06-13", + "commits": 8 + }, + { + "date": "2023-06-14", + "commits": 6 + }, + { + "date": "2023-06-15", + "commits": 2 + }, + { + "date": "2023-06-16", + "commits": 10 + }, + { + "date": "2023-06-17", + "commits": 1 + }, + { + "date": "2023-06-18", + "commits": 0 + }, + { + "date": "2023-06-19", + "commits": 5 + }, + { + "date": "2023-06-20", + "commits": 8 + }, + { + "date": "2023-06-21", + "commits": 6 + }, + { + "date": "2023-06-22", + "commits": 10 + }, + { + "date": "2023-06-23", + "commits": 10 + }, + { + "date": "2023-06-24", + "commits": 3 + }, + { + "date": "2023-06-25", + "commits": 2 + }, + { + "date": "2023-06-26", + "commits": 12 + }, + { + "date": "2023-06-27", + "commits": 9 + }, + { + "date": "2023-06-28", + "commits": 9 + }, + { + "date": "2023-06-29", + "commits": 1 + }, + { + "date": "2023-06-30", + "commits": 10 + }, + { + "date": "2023-07-01", + "commits": 5 + }, + { + "date": "2023-07-02", + "commits": 1 + }, + { + "date": "2023-07-03", + "commits": 6 + }, + { + "date": "2023-07-04", + "commits": 13 + }, + { + "date": "2023-07-05", + "commits": 2 + }, + { + "date": "2023-07-06", + "commits": 9 + }, + { + "date": "2023-07-07", + "commits": 15 + }, + { + "date": "2023-07-08", + "commits": 0 + }, + { + "date": "2023-07-09", + "commits": 4 + }, + { + "date": "2023-07-10", + "commits": 13 + }, + { + "date": "2023-07-11", + "commits": 7 + }, + { + "date": "2023-07-12", + "commits": 9 + }, + { + "date": "2023-07-13", + "commits": 10 + }, + { + "date": "2023-07-14", + "commits": 5 + }, + { + "date": "2023-07-15", + "commits": 1 + }, + { + "date": "2023-07-16", + "commits": 2 + }, + { + "date": "2023-07-17", + "commits": 10 + }, + { + "date": "2023-07-18", + "commits": 7 + }, + { + "date": "2023-07-19", + "commits": 7 + }, + { + "date": "2023-07-20", + "commits": 4 + }, + { + "date": "2023-07-21", + "commits": 6 + }, + { + "date": "2023-07-22", + "commits": 4 + }, + { + "date": "2023-07-23", + "commits": 2 + }, + { + "date": "2023-07-24", + "commits": 5 + }, + { + "date": "2023-07-25", + "commits": 5 + }, + { + "date": "2023-07-26", + "commits": 16 + }, + { + "date": "2023-07-27", + "commits": 11 + }, + { + "date": "2023-07-28", + "commits": 9 + }, + { + "date": "2023-07-29", + "commits": 0 + }, + { + "date": "2023-07-30", + "commits": 3 + }, + { + "date": "2023-07-31", + "commits": 9 + }, + { + "date": "2023-08-01", + "commits": 13 + }, + { + "date": "2023-08-02", + "commits": 9 + }, + { + "date": "2023-08-03", + "commits": 7 + }, + { + "date": "2023-08-04", + "commits": 9 + }, + { + "date": "2023-08-05", + "commits": 0 + }, + { + "date": "2023-08-06", + "commits": 5 + }, + { + "date": "2023-08-07", + "commits": 8 + }, + { + "date": "2023-08-08", + "commits": 5 + }, + { + "date": "2023-08-09", + "commits": 11 + }, + { + "date": "2023-08-10", + "commits": 13 + }, + { + "date": "2023-08-11", + "commits": 3 + }, + { + "date": "2023-08-12", + "commits": 0 + }, + { + "date": "2023-08-13", + "commits": 2 + }, + { + "date": "2023-08-14", + "commits": 8 + }, + { + "date": "2023-08-15", + "commits": 6 + }, + { + "date": "2023-08-16", + "commits": 5 + }, + { + "date": "2023-08-17", + "commits": 14 + }, + { + "date": "2023-08-18", + "commits": 11 + }, + { + "date": "2023-08-19", + "commits": 0 + }, + { + "date": "2023-08-20", + "commits": 0 + }, + { + "date": "2023-08-21", + "commits": 13 + }, + { + "date": "2023-08-22", + "commits": 10 + }, + { + "date": "2023-08-23", + "commits": 13 + }, + { + "date": "2023-08-24", + "commits": 10 + }, + { + "date": "2023-08-25", + "commits": 5 + }, + { + "date": "2023-08-26", + "commits": 2 + }, + { + "date": "2023-08-27", + "commits": 0 + }, + { + "date": "2023-08-28", + "commits": 5 + }, + { + "date": "2023-08-29", + "commits": 7 + }, + { + "date": "2023-08-30", + "commits": 9 + }, + { + "date": "2023-08-31", + "commits": 5 + }, + { + "date": "2023-09-01", + "commits": 7 + }, + { + "date": "2023-09-02", + "commits": 3 + }, + { + "date": "2023-09-03", + "commits": 3 + }, + { + "date": "2023-09-04", + "commits": 9 + }, + { + "date": "2023-09-05", + "commits": 8 + }, + { + "date": "2023-09-06", + "commits": 7 + }, + { + "date": "2023-09-07", + "commits": 10 + }, + { + "date": "2023-09-08", + "commits": 8 + }, + { + "date": "2023-09-09", + "commits": 0 + }, + { + "date": "2023-09-10", + "commits": 0 + }, + { + "date": "2023-09-11", + "commits": 7 + }, + { + "date": "2023-09-12", + "commits": 7 + }, + { + "date": "2023-09-13", + "commits": 8 + }, + { + "date": "2023-09-14", + "commits": 7 + }, + { + "date": "2023-09-15", + "commits": 8 + }, + { + "date": "2023-09-16", + "commits": 1 + }, + { + "date": "2023-09-17", + "commits": 0 + }, + { + "date": "2023-09-18", + "commits": 9 + }, + { + "date": "2023-09-19", + "commits": 11 + }, + { + "date": "2023-09-20", + "commits": 9 + }, + { + "date": "2023-09-21", + "commits": 7 + }, + { + "date": "2023-09-22", + "commits": 9 + }, + { + "date": "2023-09-23", + "commits": 0 + }, + { + "date": "2023-09-24", + "commits": 0 + }, + { + "date": "2023-09-25", + "commits": 8 + }, + { + "date": "2023-09-26", + "commits": 5 + }, + { + "date": "2023-09-27", + "commits": 10 + }, + { + "date": "2023-09-28", + "commits": 4 + }, + { + "date": "2023-09-29", + "commits": 0 + }, + { + "date": "2023-09-30", + "commits": 0 + }, + { + "date": "2023-10-01", + "commits": 6 + }, + { + "date": "2023-10-02", + "commits": 6 + }, + { + "date": "2023-10-03", + "commits": 3 + }, + { + "date": "2023-10-04", + "commits": 5 + }, + { + "date": "2023-10-05", + "commits": 9 + }, + { + "date": "2023-10-06", + "commits": 9 + }, + { + "date": "2023-10-07", + "commits": 2 + }, + { + "date": "2023-10-08", + "commits": 6 + }, + { + "date": "2023-10-09", + "commits": 10 + }, + { + "date": "2023-10-10", + "commits": 7 + }, + { + "date": "2023-10-11", + "commits": 9 + }, + { + "date": "2023-10-12", + "commits": 12 + }, + { + "date": "2023-10-13", + "commits": 10 + }, + { + "date": "2023-10-14", + "commits": 5 + }, + { + "date": "2023-10-15", + "commits": 0 + }, + { + "date": "2023-10-16", + "commits": 7 + }, + { + "date": "2023-10-17", + "commits": 10 + }, + { + "date": "2023-10-18", + "commits": 7 + }, + { + "date": "2023-10-19", + "commits": 11 + }, + { + "date": "2023-10-20", + "commits": 9 + }, + { + "date": "2023-10-21", + "commits": 4 + }, + { + "date": "2023-10-22", + "commits": 1 + }, + { + "date": "2023-10-23", + "commits": 15 + }, + { + "date": "2023-10-24", + "commits": 11 + }, + { + "date": "2023-10-25", + "commits": 7 + }, + { + "date": "2023-10-26", + "commits": 8 + }, + { + "date": "2023-10-27", + "commits": 15 + }, + { + "date": "2023-10-28", + "commits": 0 + }, + { + "date": "2023-10-29", + "commits": 4 + }, + { + "date": "2023-10-30", + "commits": 10 + }, + { + "date": "2023-10-31", + "commits": 8 + }, + { + "date": "2023-11-01", + "commits": 4 + }, + { + "date": "2023-11-02", + "commits": 8 + }, + { + "date": "2023-11-03", + "commits": 9 + }, + { + "date": "2023-11-04", + "commits": 5 + }, + { + "date": "2023-11-05", + "commits": 4 + }, + { + "date": "2023-11-06", + "commits": 8 + }, + { + "date": "2023-11-07", + "commits": 10 + }, + { + "date": "2023-11-08", + "commits": 9 + }, + { + "date": "2023-11-09", + "commits": 8 + }, + { + "date": "2023-11-10", + "commits": 8 + }, + { + "date": "2023-11-11", + "commits": 1 + }, + { + "date": "2023-11-12", + "commits": 4 + }, + { + "date": "2023-11-13", + "commits": 4 + }, + { + "date": "2023-11-14", + "commits": 6 + }, + { + "date": "2023-11-15", + "commits": 8 + }, + { + "date": "2023-11-16", + "commits": 3 + }, + { + "date": "2023-11-17", + "commits": 6 + }, + { + "date": "2023-11-18", + "commits": 0 + }, + { + "date": "2023-11-19", + "commits": 0 + }, + { + "date": "2023-11-20", + "commits": 9 + }, + { + "date": "2023-11-21", + "commits": 9 + }, + { + "date": "2023-11-22", + "commits": 7 + }, + { + "date": "2023-11-23", + "commits": 7 + }, + { + "date": "2023-11-24", + "commits": 3 + }, + { + "date": "2023-11-25", + "commits": 7 + }, + { + "date": "2023-11-26", + "commits": 3 + }, + { + "date": "2023-11-27", + "commits": 11 + }, + { + "date": "2023-11-28", + "commits": 5 + }, + { + "date": "2023-11-29", + "commits": 7 + }, + { + "date": "2023-11-30", + "commits": 2 + }, + { + "date": "2023-12-01", + "commits": 10 + }, + { + "date": "2023-12-02", + "commits": 4 + }, + { + "date": "2023-12-03", + "commits": 0 + }, + { + "date": "2023-12-04", + "commits": 7 + }, + { + "date": "2023-12-05", + "commits": 9 + }, + { + "date": "2023-12-06", + "commits": 2 + }, + { + "date": "2023-12-07", + "commits": 2 + }, + { + "date": "2023-12-08", + "commits": 4 + }, + { + "date": "2023-12-09", + "commits": 0 + }, + { + "date": "2023-12-10", + "commits": 0 + }, + { + "date": "2023-12-11", + "commits": 8 + }, + { + "date": "2023-12-12", + "commits": 8 + }, + { + "date": "2023-12-13", + "commits": 9 + }, + { + "date": "2023-12-14", + "commits": 10 + }, + { + "date": "2023-12-15", + "commits": 12 + }, + { + "date": "2023-12-16", + "commits": 5 + }, + { + "date": "2023-12-17", + "commits": 0 + }, + { + "date": "2023-12-18", + "commits": 6 + }, + { + "date": "2023-12-19", + "commits": 4 + }, + { + "date": "2023-12-20", + "commits": 4 + }, + { + "date": "2023-12-21", + "commits": 7 + }, + { + "date": "2023-12-22", + "commits": 8 + }, + { + "date": "2023-12-23", + "commits": 3 + }, + { + "date": "2023-12-24", + "commits": 0 + }, + { + "date": "2023-12-25", + "commits": 4 + }, + { + "date": "2023-12-26", + "commits": 7 + }, + { + "date": "2023-12-27", + "commits": 7 + }, + { + "date": "2023-12-28", + "commits": 7 + }, + { + "date": "2023-12-29", + "commits": 7 + }, + { + "date": "2023-12-30", + "commits": 0 + }, + { + "date": "2023-12-31", + "commits": 4 + } + ] + }, + "semantic_types": { + "date": "Date", + "commits": "Quantity" + }, + "chart_spec": { + "chartType": "Calendar Heatmap", + "encodings": { + "x": { + "field": "date" + }, + "color": { + "field": "commits" + } + }, + "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 b69ed2c3..cfc30169 100644 --- a/recursive/echarts-testing/test_cases/manifest.json +++ b/recursive/echarts-testing/test_cases/manifest.json @@ -222,5 +222,29 @@ { "test_id": "waterfall_basic", "description": "Basic waterfall chart" + }, + { + "test_id": "cal_quarter", + "description": "Calendar heatmap, ~120 daily values (one quarter)" + }, + { + "test_id": "cal_year", + "description": "Calendar heatmap, full year (365 daily values)" + }, + { + "test_id": "cal_multiyear", + "description": "Calendar heatmap spanning ~18 months across years" + }, + { + "test_id": "par_cars_grouped", + "description": "Parallel coords, 4 dims, colored by Origin (3 groups)" + }, + { + "test_id": "par_cars_single", + "description": "Parallel coords, 4 dims, no color grouping" + }, + { + "test_id": "par_metrics6", + "description": "Parallel coords, 6 dims, 120 rows, 4 groups (hairball test)" } ] \ No newline at end of file diff --git a/recursive/echarts-testing/test_cases/par_cars_grouped.json b/recursive/echarts-testing/test_cases/par_cars_grouped.json new file mode 100644 index 00000000..7bdfc7ed --- /dev/null +++ b/recursive/echarts-testing/test_cases/par_cars_grouped.json @@ -0,0 +1,344 @@ +{ + "test_id": "par_cars_grouped", + "description": "Parallel coords, 4 dims, colored by Origin (3 groups)", + "input": { + "data": { + "values": [ + { + "MPG": 32.2, + "Horsepower": 73, + "Weight": 2875, + "Acceleration": 13.8, + "Origin": "Europe" + }, + { + "MPG": 33.7, + "Horsepower": 129, + "Weight": 1891, + "Acceleration": 16.9, + "Origin": "Japan" + }, + { + "MPG": 22.8, + "Horsepower": 164, + "Weight": 4014, + "Acceleration": 17.0, + "Origin": "USA" + }, + { + "MPG": 27.4, + "Horsepower": 75, + "Weight": 2038, + "Acceleration": 17.8, + "Origin": "Japan" + }, + { + "MPG": 29.2, + "Horsepower": 87, + "Weight": 2666, + "Acceleration": 16.7, + "Origin": "Japan" + }, + { + "MPG": 28.0, + "Horsepower": 164, + "Weight": 2837, + "Acceleration": 13.0, + "Origin": "USA" + }, + { + "MPG": 19.8, + "Horsepower": 128, + "Weight": 2303, + "Acceleration": 15.7, + "Origin": "Europe" + }, + { + "MPG": 28.7, + "Horsepower": 110, + "Weight": 2599, + "Acceleration": 14.3, + "Origin": "Europe" + }, + { + "MPG": 38.7, + "Horsepower": 94, + "Weight": 2232, + "Acceleration": 14.8, + "Origin": "Japan" + }, + { + "MPG": 19.0, + "Horsepower": 149, + "Weight": 3224, + "Acceleration": 14.6, + "Origin": "USA" + }, + { + "MPG": 22.3, + "Horsepower": 126, + "Weight": 4149, + "Acceleration": 14.0, + "Origin": "USA" + }, + { + "MPG": 22.1, + "Horsepower": 217, + "Weight": 3767, + "Acceleration": 15.6, + "Origin": "USA" + }, + { + "MPG": 29.8, + "Horsepower": 100, + "Weight": 1942, + "Acceleration": 14.0, + "Origin": "Japan" + }, + { + "MPG": 25.2, + "Horsepower": 128, + "Weight": 3158, + "Acceleration": 16.2, + "Origin": "Europe" + }, + { + "MPG": 24.2, + "Horsepower": 185, + "Weight": 3846, + "Acceleration": 15.2, + "Origin": "USA" + }, + { + "MPG": 29.2, + "Horsepower": 139, + "Weight": 2585, + "Acceleration": 16.4, + "Origin": "Europe" + }, + { + "MPG": 31.5, + "Horsepower": 74, + "Weight": 2076, + "Acceleration": 17.8, + "Origin": "Japan" + }, + { + "MPG": 28.0, + "Horsepower": 150, + "Weight": 3929, + "Acceleration": 14.6, + "Origin": "USA" + }, + { + "MPG": 21.0, + "Horsepower": 179, + "Weight": 3750, + "Acceleration": 12.7, + "Origin": "USA" + }, + { + "MPG": 22.2, + "Horsepower": 154, + "Weight": 3348, + "Acceleration": 15.1, + "Origin": "USA" + }, + { + "MPG": 29.4, + "Horsepower": 110, + "Weight": 2417, + "Acceleration": 16.9, + "Origin": "Japan" + }, + { + "MPG": 30.5, + "Horsepower": 92, + "Weight": 2016, + "Acceleration": 18.5, + "Origin": "Japan" + }, + { + "MPG": 24.4, + "Horsepower": 141, + "Weight": 2684, + "Acceleration": 13.2, + "Origin": "USA" + }, + { + "MPG": 17.6, + "Horsepower": 131, + "Weight": 3474, + "Acceleration": 17.7, + "Origin": "USA" + }, + { + "MPG": 33.4, + "Horsepower": 125, + "Weight": 2485, + "Acceleration": 16.0, + "Origin": "Japan" + }, + { + "MPG": 31.0, + "Horsepower": 87, + "Weight": 2656, + "Acceleration": 16.4, + "Origin": "Europe" + }, + { + "MPG": 16.9, + "Horsepower": 148, + "Weight": 3375, + "Acceleration": 13.0, + "Origin": "USA" + }, + { + "MPG": 29.4, + "Horsepower": 80, + "Weight": 2741, + "Acceleration": 17.2, + "Origin": "Europe" + }, + { + "MPG": 33.1, + "Horsepower": 94, + "Weight": 1867, + "Acceleration": 16.9, + "Origin": "Japan" + }, + { + "MPG": 28.6, + "Horsepower": 84, + "Weight": 2476, + "Acceleration": 15.8, + "Origin": "Japan" + }, + { + "MPG": 23.7, + "Horsepower": 163, + "Weight": 3401, + "Acceleration": 15.6, + "Origin": "USA" + }, + { + "MPG": 23.5, + "Horsepower": 134, + "Weight": 3025, + "Acceleration": 17.4, + "Origin": "Europe" + }, + { + "MPG": 32.8, + "Horsepower": 106, + "Weight": 2633, + "Acceleration": 13.5, + "Origin": "Europe" + }, + { + "MPG": 12.3, + "Horsepower": 114, + "Weight": 3028, + "Acceleration": 15.3, + "Origin": "USA" + }, + { + "MPG": 27.8, + "Horsepower": 109, + "Weight": 2537, + "Acceleration": 14.3, + "Origin": "Europe" + }, + { + "MPG": 30.2, + "Horsepower": 105, + "Weight": 2596, + "Acceleration": 17.5, + "Origin": "Japan" + }, + { + "MPG": 26.3, + "Horsepower": 57, + "Weight": 2756, + "Acceleration": 12.9, + "Origin": "Europe" + }, + { + "MPG": 23.4, + "Horsepower": 140, + "Weight": 3375, + "Acceleration": 13.6, + "Origin": "Europe" + }, + { + "MPG": 27.9, + "Horsepower": 108, + "Weight": 2740, + "Acceleration": 16.0, + "Origin": "Europe" + }, + { + "MPG": 27.1, + "Horsepower": 113, + "Weight": 2205, + "Acceleration": 16.0, + "Origin": "Japan" + }, + { + "MPG": 33.3, + "Horsepower": 105, + "Weight": 2068, + "Acceleration": 17.4, + "Origin": "Japan" + }, + { + "MPG": 29.5, + "Horsepower": 74, + "Weight": 1936, + "Acceleration": 17.1, + "Origin": "Japan" + }, + { + "MPG": 31.0, + "Horsepower": 90, + "Weight": 2024, + "Acceleration": 15.1, + "Origin": "Japan" + }, + { + "MPG": 21.9, + "Horsepower": 146, + "Weight": 3510, + "Acceleration": 12.1, + "Origin": "USA" + }, + { + "MPG": 27.0, + "Horsepower": 114, + "Weight": 2634, + "Acceleration": 18.9, + "Origin": "Europe" + } + ] + }, + "semantic_types": { + "MPG": "Quantity", + "Horsepower": "Quantity", + "Weight": "Quantity", + "Acceleration": "Quantity", + "Origin": "Category" + }, + "chart_spec": { + "chartType": "Parallel Coordinates", + "encodings": { + "color": { + "field": "Origin" + } + }, + "canvasSize": { + "width": 480, + "height": 320 + } + } + } +} \ No newline at end of file diff --git a/recursive/echarts-testing/test_cases/par_cars_single.json b/recursive/echarts-testing/test_cases/par_cars_single.json new file mode 100644 index 00000000..4b5016c5 --- /dev/null +++ b/recursive/echarts-testing/test_cases/par_cars_single.json @@ -0,0 +1,294 @@ +{ + "test_id": "par_cars_single", + "description": "Parallel coords, 4 dims, no color grouping", + "input": { + "data": { + "values": [ + { + "MPG": 32.2, + "Horsepower": 73, + "Weight": 2875, + "Acceleration": 13.8 + }, + { + "MPG": 33.7, + "Horsepower": 129, + "Weight": 1891, + "Acceleration": 16.9 + }, + { + "MPG": 22.8, + "Horsepower": 164, + "Weight": 4014, + "Acceleration": 17.0 + }, + { + "MPG": 27.4, + "Horsepower": 75, + "Weight": 2038, + "Acceleration": 17.8 + }, + { + "MPG": 29.2, + "Horsepower": 87, + "Weight": 2666, + "Acceleration": 16.7 + }, + { + "MPG": 28.0, + "Horsepower": 164, + "Weight": 2837, + "Acceleration": 13.0 + }, + { + "MPG": 19.8, + "Horsepower": 128, + "Weight": 2303, + "Acceleration": 15.7 + }, + { + "MPG": 28.7, + "Horsepower": 110, + "Weight": 2599, + "Acceleration": 14.3 + }, + { + "MPG": 38.7, + "Horsepower": 94, + "Weight": 2232, + "Acceleration": 14.8 + }, + { + "MPG": 19.0, + "Horsepower": 149, + "Weight": 3224, + "Acceleration": 14.6 + }, + { + "MPG": 22.3, + "Horsepower": 126, + "Weight": 4149, + "Acceleration": 14.0 + }, + { + "MPG": 22.1, + "Horsepower": 217, + "Weight": 3767, + "Acceleration": 15.6 + }, + { + "MPG": 29.8, + "Horsepower": 100, + "Weight": 1942, + "Acceleration": 14.0 + }, + { + "MPG": 25.2, + "Horsepower": 128, + "Weight": 3158, + "Acceleration": 16.2 + }, + { + "MPG": 24.2, + "Horsepower": 185, + "Weight": 3846, + "Acceleration": 15.2 + }, + { + "MPG": 29.2, + "Horsepower": 139, + "Weight": 2585, + "Acceleration": 16.4 + }, + { + "MPG": 31.5, + "Horsepower": 74, + "Weight": 2076, + "Acceleration": 17.8 + }, + { + "MPG": 28.0, + "Horsepower": 150, + "Weight": 3929, + "Acceleration": 14.6 + }, + { + "MPG": 21.0, + "Horsepower": 179, + "Weight": 3750, + "Acceleration": 12.7 + }, + { + "MPG": 22.2, + "Horsepower": 154, + "Weight": 3348, + "Acceleration": 15.1 + }, + { + "MPG": 29.4, + "Horsepower": 110, + "Weight": 2417, + "Acceleration": 16.9 + }, + { + "MPG": 30.5, + "Horsepower": 92, + "Weight": 2016, + "Acceleration": 18.5 + }, + { + "MPG": 24.4, + "Horsepower": 141, + "Weight": 2684, + "Acceleration": 13.2 + }, + { + "MPG": 17.6, + "Horsepower": 131, + "Weight": 3474, + "Acceleration": 17.7 + }, + { + "MPG": 33.4, + "Horsepower": 125, + "Weight": 2485, + "Acceleration": 16.0 + }, + { + "MPG": 31.0, + "Horsepower": 87, + "Weight": 2656, + "Acceleration": 16.4 + }, + { + "MPG": 16.9, + "Horsepower": 148, + "Weight": 3375, + "Acceleration": 13.0 + }, + { + "MPG": 29.4, + "Horsepower": 80, + "Weight": 2741, + "Acceleration": 17.2 + }, + { + "MPG": 33.1, + "Horsepower": 94, + "Weight": 1867, + "Acceleration": 16.9 + }, + { + "MPG": 28.6, + "Horsepower": 84, + "Weight": 2476, + "Acceleration": 15.8 + }, + { + "MPG": 23.7, + "Horsepower": 163, + "Weight": 3401, + "Acceleration": 15.6 + }, + { + "MPG": 23.5, + "Horsepower": 134, + "Weight": 3025, + "Acceleration": 17.4 + }, + { + "MPG": 32.8, + "Horsepower": 106, + "Weight": 2633, + "Acceleration": 13.5 + }, + { + "MPG": 12.3, + "Horsepower": 114, + "Weight": 3028, + "Acceleration": 15.3 + }, + { + "MPG": 27.8, + "Horsepower": 109, + "Weight": 2537, + "Acceleration": 14.3 + }, + { + "MPG": 30.2, + "Horsepower": 105, + "Weight": 2596, + "Acceleration": 17.5 + }, + { + "MPG": 26.3, + "Horsepower": 57, + "Weight": 2756, + "Acceleration": 12.9 + }, + { + "MPG": 23.4, + "Horsepower": 140, + "Weight": 3375, + "Acceleration": 13.6 + }, + { + "MPG": 27.9, + "Horsepower": 108, + "Weight": 2740, + "Acceleration": 16.0 + }, + { + "MPG": 27.1, + "Horsepower": 113, + "Weight": 2205, + "Acceleration": 16.0 + }, + { + "MPG": 33.3, + "Horsepower": 105, + "Weight": 2068, + "Acceleration": 17.4 + }, + { + "MPG": 29.5, + "Horsepower": 74, + "Weight": 1936, + "Acceleration": 17.1 + }, + { + "MPG": 31.0, + "Horsepower": 90, + "Weight": 2024, + "Acceleration": 15.1 + }, + { + "MPG": 21.9, + "Horsepower": 146, + "Weight": 3510, + "Acceleration": 12.1 + }, + { + "MPG": 27.0, + "Horsepower": 114, + "Weight": 2634, + "Acceleration": 18.9 + } + ] + }, + "semantic_types": { + "MPG": "Quantity", + "Horsepower": "Quantity", + "Weight": "Quantity", + "Acceleration": "Quantity" + }, + "chart_spec": { + "chartType": "Parallel Coordinates", + "encodings": {}, + "canvasSize": { + "width": 480, + "height": 320 + } + } + } +} \ No newline at end of file diff --git a/recursive/echarts-testing/test_cases/par_metrics6.json b/recursive/echarts-testing/test_cases/par_metrics6.json new file mode 100644 index 00000000..15840954 --- /dev/null +++ b/recursive/echarts-testing/test_cases/par_metrics6.json @@ -0,0 +1,1111 @@ +{ + "test_id": "par_metrics6", + "description": "Parallel coords, 6 dims, 120 rows, 4 groups (hairball test)", + "input": { + "data": { + "values": [ + { + "Speed": 29.1, + "Power": 91.0, + "Range": 80.2, + "Comfort": 89.0, + "Price": 65.2, + "Safety": 66.6, + "Tier": "C" + }, + { + "Speed": 64.1, + "Power": 18.7, + "Range": 7.1, + "Comfort": 84.6, + "Price": 74.5, + "Safety": 3.0, + "Tier": "B" + }, + { + "Speed": 83.8, + "Power": 60.8, + "Range": 1.4, + "Comfort": 27.6, + "Price": 14.7, + "Safety": 87.1, + "Tier": "B" + }, + { + "Speed": 82.6, + "Power": 74.5, + "Range": 94.9, + "Comfort": 79.4, + "Price": 25.7, + "Safety": 85.0, + "Tier": "C" + }, + { + "Speed": 89.8, + "Power": 55.0, + "Range": 98.1, + "Comfort": 67.1, + "Price": 88.9, + "Safety": 63.9, + "Tier": "D" + }, + { + "Speed": 34.6, + "Power": 59.5, + "Range": 63.4, + "Comfort": 26.3, + "Price": 44.8, + "Safety": 61.1, + "Tier": "A" + }, + { + "Speed": 44.0, + "Power": 72.9, + "Range": 53.6, + "Comfort": 29.3, + "Price": 17.9, + "Safety": 35.6, + "Tier": "B" + }, + { + "Speed": 45.9, + "Power": 60.5, + "Range": 39.6, + "Comfort": 13.1, + "Price": 49.1, + "Safety": 23.5, + "Tier": "C" + }, + { + "Speed": 88.7, + "Power": 70.2, + "Range": 13.1, + "Comfort": 43.0, + "Price": 62.9, + "Safety": 46.4, + "Tier": "B" + }, + { + "Speed": 71.9, + "Power": 93.3, + "Range": 4.3, + "Comfort": 14.2, + "Price": 80.6, + "Safety": 83.8, + "Tier": "A" + }, + { + "Speed": 16.0, + "Power": 41.3, + "Range": 86.8, + "Comfort": 88.6, + "Price": 80.3, + "Safety": 20.3, + "Tier": "C" + }, + { + "Speed": 52.0, + "Power": 65.7, + "Range": 90.1, + "Comfort": 70.4, + "Price": 23.8, + "Safety": 49.6, + "Tier": "C" + }, + { + "Speed": 58.1, + "Power": 59.1, + "Range": 35.9, + "Comfort": 72.8, + "Price": 34.1, + "Safety": 29.5, + "Tier": "A" + }, + { + "Speed": 78.7, + "Power": 55.5, + "Range": 90.1, + "Comfort": 16.7, + "Price": 42.9, + "Safety": 59.5, + "Tier": "B" + }, + { + "Speed": 97.1, + "Power": 82.5, + "Range": 64.5, + "Comfort": 52.7, + "Price": 12.6, + "Safety": 31.1, + "Tier": "A" + }, + { + "Speed": 30.2, + "Power": 26.9, + "Range": 59.7, + "Comfort": 7.7, + "Price": 47.0, + "Safety": 36.7, + "Tier": "D" + }, + { + "Speed": 0.6, + "Power": 33.5, + "Range": 49.7, + "Comfort": 97.0, + "Price": 51.7, + "Safety": 47.4, + "Tier": "A" + }, + { + "Speed": 29.7, + "Power": 3.2, + "Range": 50.1, + "Comfort": 91.0, + "Price": 73.8, + "Safety": 25.8, + "Tier": "B" + }, + { + "Speed": 25.9, + "Power": 58.8, + "Range": 35.6, + "Comfort": 24.4, + "Price": 28.7, + "Safety": 20.4, + "Tier": "B" + }, + { + "Speed": 42.5, + "Power": 31.7, + "Range": 46.1, + "Comfort": 6.9, + "Price": 4.6, + "Safety": 92.1, + "Tier": "C" + }, + { + "Speed": 19.0, + "Power": 48.1, + "Range": 55.5, + "Comfort": 33.3, + "Price": 28.9, + "Safety": 43.5, + "Tier": "C" + }, + { + "Speed": 14.8, + "Power": 5.3, + "Range": 36.9, + "Comfort": 15.6, + "Price": 30.8, + "Safety": 60.3, + "Tier": "B" + }, + { + "Speed": 3.3, + "Power": 12.8, + "Range": 36.1, + "Comfort": 44.2, + "Price": 87.6, + "Safety": 99.0, + "Tier": "C" + }, + { + "Speed": 35.1, + "Power": 29.5, + "Range": 96.8, + "Comfort": 4.9, + "Price": 16.7, + "Safety": 88.7, + "Tier": "A" + }, + { + "Speed": 90.2, + "Power": 89.5, + "Range": 91.2, + "Comfort": 68.2, + "Price": 9.0, + "Safety": 93.2, + "Tier": "B" + }, + { + "Speed": 34.5, + "Power": 80.2, + "Range": 21.4, + "Comfort": 71.8, + "Price": 66.7, + "Safety": 93.5, + "Tier": "A" + }, + { + "Speed": 19.5, + "Power": 39.3, + "Range": 5.4, + "Comfort": 54.7, + "Price": 5.2, + "Safety": 61.3, + "Tier": "D" + }, + { + "Speed": 53.4, + "Power": 30.0, + "Range": 96.2, + "Comfort": 29.8, + "Price": 8.7, + "Safety": 5.2, + "Tier": "C" + }, + { + "Speed": 16.7, + "Power": 1.6, + "Range": 52.4, + "Comfort": 27.1, + "Price": 9.1, + "Safety": 33.4, + "Tier": "A" + }, + { + "Speed": 16.1, + "Power": 97.9, + "Range": 67.4, + "Comfort": 70.1, + "Price": 91.8, + "Safety": 4.1, + "Tier": "D" + }, + { + "Speed": 30.8, + "Power": 99.6, + "Range": 47.1, + "Comfort": 26.8, + "Price": 78.5, + "Safety": 45.0, + "Tier": "A" + }, + { + "Speed": 52.6, + "Power": 53.2, + "Range": 27.6, + "Comfort": 22.6, + "Price": 99.2, + "Safety": 95.4, + "Tier": "A" + }, + { + "Speed": 41.6, + "Power": 99.3, + "Range": 76.2, + "Comfort": 48.4, + "Price": 26.1, + "Safety": 46.1, + "Tier": "C" + }, + { + "Speed": 19.4, + "Power": 37.6, + "Range": 47.6, + "Comfort": 19.1, + "Price": 57.3, + "Safety": 3.4, + "Tier": "D" + }, + { + "Speed": 12.5, + "Power": 0.6, + "Range": 13.3, + "Comfort": 8.1, + "Price": 90.4, + "Safety": 57.1, + "Tier": "B" + }, + { + "Speed": 21.4, + "Power": 74.5, + "Range": 64.9, + "Comfort": 42.8, + "Price": 95.9, + "Safety": 2.2, + "Tier": "A" + }, + { + "Speed": 29.8, + "Power": 61.9, + "Range": 65.0, + "Comfort": 73.2, + "Price": 42.8, + "Safety": 75.1, + "Tier": "B" + }, + { + "Speed": 48.6, + "Power": 60.8, + "Range": 24.4, + "Comfort": 53.0, + "Price": 1.0, + "Safety": 44.9, + "Tier": "B" + }, + { + "Speed": 65.3, + "Power": 60.8, + "Range": 52.5, + "Comfort": 99.3, + "Price": 23.5, + "Safety": 51.0, + "Tier": "C" + }, + { + "Speed": 10.0, + "Power": 91.4, + "Range": 93.2, + "Comfort": 81.7, + "Price": 83.1, + "Safety": 68.1, + "Tier": "A" + }, + { + "Speed": 66.7, + "Power": 59.1, + "Range": 25.7, + "Comfort": 13.4, + "Price": 72.6, + "Safety": 59.7, + "Tier": "B" + }, + { + "Speed": 54.7, + "Power": 14.2, + "Range": 73.5, + "Comfort": 2.6, + "Price": 46.2, + "Safety": 35.6, + "Tier": "A" + }, + { + "Speed": 80.5, + "Power": 51.3, + "Range": 6.7, + "Comfort": 85.9, + "Price": 52.7, + "Safety": 28.0, + "Tier": "D" + }, + { + "Speed": 67.2, + "Power": 91.3, + "Range": 68.4, + "Comfort": 37.5, + "Price": 7.8, + "Safety": 47.8, + "Tier": "D" + }, + { + "Speed": 30.0, + "Power": 92.8, + "Range": 2.6, + "Comfort": 71.3, + "Price": 39.0, + "Safety": 48.1, + "Tier": "D" + }, + { + "Speed": 8.0, + "Power": 21.5, + "Range": 14.6, + "Comfort": 32.5, + "Price": 30.0, + "Safety": 96.8, + "Tier": "A" + }, + { + "Speed": 43.1, + "Power": 56.2, + "Range": 1.1, + "Comfort": 62.9, + "Price": 87.6, + "Safety": 75.0, + "Tier": "B" + }, + { + "Speed": 95.6, + "Power": 55.5, + "Range": 62.8, + "Comfort": 31.3, + "Price": 99.2, + "Safety": 16.0, + "Tier": "B" + }, + { + "Speed": 4.9, + "Power": 23.1, + "Range": 56.5, + "Comfort": 88.4, + "Price": 65.9, + "Safety": 70.9, + "Tier": "B" + }, + { + "Speed": 79.7, + "Power": 85.3, + "Range": 15.5, + "Comfort": 5.4, + "Price": 62.1, + "Safety": 51.9, + "Tier": "A" + }, + { + "Speed": 9.2, + "Power": 77.5, + "Range": 30.5, + "Comfort": 29.7, + "Price": 26.2, + "Safety": 80.6, + "Tier": "B" + }, + { + "Speed": 90.7, + "Power": 28.0, + "Range": 71.6, + "Comfort": 9.7, + "Price": 65.2, + "Safety": 16.1, + "Tier": "D" + }, + { + "Speed": 96.5, + "Power": 25.4, + "Range": 80.8, + "Comfort": 22.0, + "Price": 30.5, + "Safety": 10.3, + "Tier": "A" + }, + { + "Speed": 59.9, + "Power": 4.6, + "Range": 89.5, + "Comfort": 54.5, + "Price": 13.7, + "Safety": 47.8, + "Tier": "D" + }, + { + "Speed": 33.2, + "Power": 36.4, + "Range": 0.2, + "Comfort": 88.7, + "Price": 8.8, + "Safety": 10.1, + "Tier": "D" + }, + { + "Speed": 44.8, + "Power": 42.1, + "Range": 13.9, + "Comfort": 0.1, + "Price": 52.0, + "Safety": 24.4, + "Tier": "C" + }, + { + "Speed": 42.9, + "Power": 43.6, + "Range": 35.7, + "Comfort": 32.8, + "Price": 34.9, + "Safety": 19.3, + "Tier": "B" + }, + { + "Speed": 74.2, + "Power": 56.3, + "Range": 63.0, + "Comfort": 94.3, + "Price": 79.9, + "Safety": 59.9, + "Tier": "B" + }, + { + "Speed": 49.2, + "Power": 55.2, + "Range": 81.7, + "Comfort": 61.9, + "Price": 37.6, + "Safety": 2.0, + "Tier": "A" + }, + { + "Speed": 34.0, + "Power": 42.9, + "Range": 28.8, + "Comfort": 55.5, + "Price": 40.1, + "Safety": 83.4, + "Tier": "D" + }, + { + "Speed": 78.3, + "Power": 44.0, + "Range": 53.6, + "Comfort": 75.0, + "Price": 72.3, + "Safety": 74.4, + "Tier": "A" + }, + { + "Speed": 55.5, + "Power": 18.8, + "Range": 24.1, + "Comfort": 43.5, + "Price": 49.9, + "Safety": 89.9, + "Tier": "A" + }, + { + "Speed": 77.5, + "Power": 19.1, + "Range": 90.8, + "Comfort": 40.0, + "Price": 61.6, + "Safety": 44.0, + "Tier": "A" + }, + { + "Speed": 43.1, + "Power": 56.8, + "Range": 5.7, + "Comfort": 40.3, + "Price": 80.3, + "Safety": 73.5, + "Tier": "B" + }, + { + "Speed": 20.5, + "Power": 3.7, + "Range": 1.7, + "Comfort": 52.5, + "Price": 68.5, + "Safety": 39.3, + "Tier": "D" + }, + { + "Speed": 86.6, + "Power": 45.3, + "Range": 49.2, + "Comfort": 49.0, + "Price": 62.9, + "Safety": 8.1, + "Tier": "B" + }, + { + "Speed": 6.0, + "Power": 79.8, + "Range": 63.9, + "Comfort": 24.0, + "Price": 11.4, + "Safety": 42.0, + "Tier": "A" + }, + { + "Speed": 46.5, + "Power": 3.6, + "Range": 82.9, + "Comfort": 20.2, + "Price": 73.1, + "Safety": 49.9, + "Tier": "B" + }, + { + "Speed": 14.8, + "Power": 79.4, + "Range": 72.0, + "Comfort": 1.7, + "Price": 56.5, + "Safety": 29.5, + "Tier": "B" + }, + { + "Speed": 96.1, + "Power": 66.6, + "Range": 49.4, + "Comfort": 60.4, + "Price": 24.9, + "Safety": 46.8, + "Tier": "D" + }, + { + "Speed": 6.5, + "Power": 59.5, + "Range": 38.8, + "Comfort": 69.7, + "Price": 19.2, + "Safety": 91.5, + "Tier": "A" + }, + { + "Speed": 38.2, + "Power": 20.5, + "Range": 99.9, + "Comfort": 45.1, + "Price": 80.4, + "Safety": 32.8, + "Tier": "A" + }, + { + "Speed": 62.9, + "Power": 63.6, + "Range": 17.5, + "Comfort": 14.0, + "Price": 24.0, + "Safety": 52.3, + "Tier": "D" + }, + { + "Speed": 50.4, + "Power": 43.0, + "Range": 81.2, + "Comfort": 40.0, + "Price": 41.4, + "Safety": 77.8, + "Tier": "A" + }, + { + "Speed": 45.1, + "Power": 75.1, + "Range": 10.7, + "Comfort": 44.2, + "Price": 23.5, + "Safety": 67.1, + "Tier": "C" + }, + { + "Speed": 26.4, + "Power": 72.0, + "Range": 46.4, + "Comfort": 21.8, + "Price": 27.6, + "Safety": 53.8, + "Tier": "B" + }, + { + "Speed": 66.2, + "Power": 92.1, + "Range": 12.8, + "Comfort": 33.0, + "Price": 98.3, + "Safety": 1.8, + "Tier": "A" + }, + { + "Speed": 27.1, + "Power": 13.8, + "Range": 87.9, + "Comfort": 44.2, + "Price": 7.1, + "Safety": 89.1, + "Tier": "D" + }, + { + "Speed": 93.7, + "Power": 8.5, + "Range": 76.5, + "Comfort": 77.1, + "Price": 99.2, + "Safety": 74.6, + "Tier": "A" + }, + { + "Speed": 87.9, + "Power": 46.7, + "Range": 71.5, + "Comfort": 46.7, + "Price": 71.6, + "Safety": 95.8, + "Tier": "D" + }, + { + "Speed": 55.3, + "Power": 96.3, + "Range": 7.9, + "Comfort": 40.6, + "Price": 40.9, + "Safety": 89.5, + "Tier": "C" + }, + { + "Speed": 59.5, + "Power": 95.0, + "Range": 15.5, + "Comfort": 57.4, + "Price": 7.4, + "Safety": 58.3, + "Tier": "A" + }, + { + "Speed": 28.5, + "Power": 81.2, + "Range": 41.7, + "Comfort": 19.7, + "Price": 19.3, + "Safety": 57.8, + "Tier": "D" + }, + { + "Speed": 30.2, + "Power": 7.8, + "Range": 45.1, + "Comfort": 19.7, + "Price": 0.8, + "Safety": 4.7, + "Tier": "D" + }, + { + "Speed": 47.3, + "Power": 30.6, + "Range": 64.1, + "Comfort": 60.2, + "Price": 69.0, + "Safety": 32.3, + "Tier": "A" + }, + { + "Speed": 94.6, + "Power": 2.1, + "Range": 16.6, + "Comfort": 32.9, + "Price": 79.7, + "Safety": 74.3, + "Tier": "C" + }, + { + "Speed": 0.1, + "Power": 42.3, + "Range": 68.4, + "Comfort": 42.2, + "Price": 37.0, + "Safety": 16.7, + "Tier": "D" + }, + { + "Speed": 77.0, + "Power": 67.3, + "Range": 24.9, + "Comfort": 86.7, + "Price": 51.5, + "Safety": 20.6, + "Tier": "C" + }, + { + "Speed": 31.4, + "Power": 16.9, + "Range": 24.6, + "Comfort": 28.1, + "Price": 79.4, + "Safety": 59.4, + "Tier": "A" + }, + { + "Speed": 84.2, + "Power": 63.1, + "Range": 1.2, + "Comfort": 73.8, + "Price": 19.0, + "Safety": 94.7, + "Tier": "D" + }, + { + "Speed": 55.0, + "Power": 73.2, + "Range": 13.8, + "Comfort": 41.7, + "Price": 39.4, + "Safety": 81.5, + "Tier": "A" + }, + { + "Speed": 56.0, + "Power": 6.4, + "Range": 42.4, + "Comfort": 64.9, + "Price": 86.4, + "Safety": 12.3, + "Tier": "C" + }, + { + "Speed": 10.5, + "Power": 20.6, + "Range": 33.4, + "Comfort": 16.9, + "Price": 9.1, + "Safety": 65.5, + "Tier": "B" + }, + { + "Speed": 24.8, + "Power": 59.2, + "Range": 31.9, + "Comfort": 7.9, + "Price": 32.8, + "Safety": 41.2, + "Tier": "D" + }, + { + "Speed": 38.7, + "Power": 7.5, + "Range": 56.8, + "Comfort": 50.8, + "Price": 26.2, + "Safety": 78.2, + "Tier": "D" + }, + { + "Speed": 48.0, + "Power": 6.8, + "Range": 14.5, + "Comfort": 84.1, + "Price": 35.3, + "Safety": 83.0, + "Tier": "D" + }, + { + "Speed": 62.5, + "Power": 35.6, + "Range": 58.0, + "Comfort": 41.5, + "Price": 51.7, + "Safety": 94.5, + "Tier": "A" + }, + { + "Speed": 65.4, + "Power": 95.5, + "Range": 72.0, + "Comfort": 39.8, + "Price": 63.2, + "Safety": 50.4, + "Tier": "A" + }, + { + "Speed": 12.4, + "Power": 84.8, + "Range": 39.6, + "Comfort": 48.9, + "Price": 52.0, + "Safety": 20.6, + "Tier": "B" + }, + { + "Speed": 86.2, + "Power": 44.9, + "Range": 43.9, + "Comfort": 36.7, + "Price": 19.1, + "Safety": 28.5, + "Tier": "A" + }, + { + "Speed": 96.5, + "Power": 37.5, + "Range": 28.7, + "Comfort": 33.7, + "Price": 86.4, + "Safety": 26.2, + "Tier": "C" + }, + { + "Speed": 4.4, + "Power": 93.7, + "Range": 75.9, + "Comfort": 96.7, + "Price": 75.5, + "Safety": 86.5, + "Tier": "B" + }, + { + "Speed": 90.8, + "Power": 86.2, + "Range": 36.6, + "Comfort": 47.8, + "Price": 17.7, + "Safety": 38.0, + "Tier": "C" + }, + { + "Speed": 35.1, + "Power": 41.5, + "Range": 2.0, + "Comfort": 26.0, + "Price": 17.7, + "Safety": 23.9, + "Tier": "D" + }, + { + "Speed": 84.7, + "Power": 0.9, + "Range": 10.7, + "Comfort": 23.8, + "Price": 99.6, + "Safety": 45.4, + "Tier": "A" + }, + { + "Speed": 96.5, + "Power": 33.1, + "Range": 54.2, + "Comfort": 43.2, + "Price": 8.0, + "Safety": 42.3, + "Tier": "B" + }, + { + "Speed": 83.1, + "Power": 5.4, + "Range": 93.4, + "Comfort": 6.3, + "Price": 62.9, + "Safety": 31.1, + "Tier": "D" + }, + { + "Speed": 90.4, + "Power": 48.0, + "Range": 60.3, + "Comfort": 46.8, + "Price": 85.9, + "Safety": 73.2, + "Tier": "B" + }, + { + "Speed": 16.9, + "Power": 49.7, + "Range": 84.1, + "Comfort": 19.8, + "Price": 26.0, + "Safety": 46.8, + "Tier": "D" + }, + { + "Speed": 73.2, + "Power": 85.6, + "Range": 37.5, + "Comfort": 32.2, + "Price": 63.2, + "Safety": 3.3, + "Tier": "D" + }, + { + "Speed": 20.9, + "Power": 42.4, + "Range": 80.6, + "Comfort": 85.6, + "Price": 75.9, + "Safety": 29.3, + "Tier": "D" + }, + { + "Speed": 61.5, + "Power": 20.4, + "Range": 56.8, + "Comfort": 88.9, + "Price": 76.2, + "Safety": 40.7, + "Tier": "D" + }, + { + "Speed": 75.2, + "Power": 33.2, + "Range": 87.2, + "Comfort": 91.6, + "Price": 7.6, + "Safety": 42.5, + "Tier": "A" + }, + { + "Speed": 30.5, + "Power": 24.1, + "Range": 78.5, + "Comfort": 20.9, + "Price": 55.3, + "Safety": 2.0, + "Tier": "A" + }, + { + "Speed": 48.8, + "Power": 21.0, + "Range": 1.2, + "Comfort": 96.2, + "Price": 18.3, + "Safety": 3.8, + "Tier": "D" + }, + { + "Speed": 29.5, + "Power": 29.8, + "Range": 73.4, + "Comfort": 58.0, + "Price": 44.6, + "Safety": 69.1, + "Tier": "B" + }, + { + "Speed": 29.6, + "Power": 23.3, + "Range": 3.6, + "Comfort": 1.2, + "Price": 27.4, + "Safety": 63.4, + "Tier": "A" + }, + { + "Speed": 52.2, + "Power": 14.3, + "Range": 10.4, + "Comfort": 67.1, + "Price": 34.1, + "Safety": 52.6, + "Tier": "D" + }, + { + "Speed": 27.0, + "Power": 16.4, + "Range": 32.0, + "Comfort": 63.6, + "Price": 38.4, + "Safety": 22.7, + "Tier": "A" + }, + { + "Speed": 56.2, + "Power": 32.7, + "Range": 7.7, + "Comfort": 17.6, + "Price": 3.9, + "Safety": 89.8, + "Tier": "C" + } + ] + }, + "semantic_types": { + "Speed": "Quantity", + "Power": "Quantity", + "Range": "Quantity", + "Comfort": "Quantity", + "Price": "Quantity", + "Safety": "Quantity", + "Tier": "Category" + }, + "chart_spec": { + "chartType": "Parallel Coordinates", + "encodings": { + "color": { + "field": "Tier" + } + }, + "canvasSize": { + "width": 480, + "height": 320 + } + } + } +} \ No newline at end of file diff --git a/recursive/gen_gallery_cases.mts b/recursive/gen_gallery_cases.mts new file mode 100644 index 00000000..b2817d77 --- /dev/null +++ b/recursive/gen_gallery_cases.mts @@ -0,0 +1,88 @@ +/** + * Convert gallery TEST_GENERATORS output into harness test_cases JSON. + * + * Usage: + * npx tsx recursive/gen_gallery_cases.mts "" [more keys...] + * backend = echarts | chartjs (selects the output test_cases dir) + * + * Mirrors site/src/shared/test-case-utils.ts::testCaseToAssemblyInput so the + * rendered output matches exactly what the gallery shows. + */ +import { TEST_GENERATORS } from '../packages/flint-js/src/test-data/index'; +import type { TestCase } from '../packages/flint-js/src/test-data/types'; +import * as fs from 'fs'; +import * as path from 'path'; +import { fileURLToPath } from 'url'; + +const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); + +function buildEncodings(t: TestCase): Record { + const idToName = new Map(t.fields.map((f) => [f.id, f.name])); + const encodings: Record = {}; + for (const [channel, e] of Object.entries(t.encodingMap)) { + if (!e?.fieldID) continue; + const field = idToName.get(e.fieldID) ?? e.fieldID; + encodings[channel] = { + field, + type: (e as any).dtype, + aggregate: e.aggregate, + sortOrder: e.sortOrder, + sortBy: e.sortBy, + scheme: e.scheme, + }; + } + return encodings; +} + +function toInput(t: TestCase) { + const semantic_types: Record = {}; + for (const [k, m] of Object.entries(t.metadata)) semantic_types[k] = m.semanticType; + return { + data: { values: t.data }, + semantic_types, + chart_spec: { + chartType: t.chartType, + encodings: buildEncodings(t), + canvasSize: { width: 480, height: 320 }, + chartProperties: t.chartProperties, + }, + options: t.assembleOptions, + semantic_annotations: t.semanticAnnotations, + }; +} + +function slug(s: string): string { + return s.toLowerCase().replace(/[^a-z0-9]+/g, '_').replace(/^_+|_+$/g, ''); +} + +function main() { + const [, , backend, prefix, ...keys] = process.argv; + if (!backend || !prefix || keys.length === 0) { + console.error('args: "" [more keys...]'); + process.exit(1); + } + const dir = path.join(REPO_ROOT, `recursive/${backend}-testing/test_cases`); + if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); + + const manifestPath = path.join(dir, 'manifest.json'); + const manifest: { test_id: string; description?: string }[] = + fs.existsSync(manifestPath) ? JSON.parse(fs.readFileSync(manifestPath, 'utf-8')) : []; + const existing = new Set(manifest.map((m) => m.test_id)); + + for (const key of keys) { + const gen = (TEST_GENERATORS as Record TestCase[]>)[key]; + if (!gen) { console.error(`unknown generator: ${key}`); continue; } + const cases = gen(); + cases.forEach((tc, i) => { + const testId = `${prefix}_${slug(key)}_${i}`; + const obj = { test_id: testId, description: tc.title, input: toInput(tc) }; + fs.writeFileSync(path.join(dir, `${testId}.json`), JSON.stringify(obj, null, 2)); + if (!existing.has(testId)) { manifest.push({ test_id: testId, description: tc.title }); existing.add(testId); } + console.log(`wrote ${testId} (${tc.chartType})`); + }); + } + fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2)); + console.log(`manifest now has ${manifest.length} entries`); +} + +main(); diff --git a/site/src/shared/chart-categories.ts b/site/src/shared/chart-categories.ts index d01513be..d11de170 100644 --- a/site/src/shared/chart-categories.ts +++ b/site/src/shared/chart-categories.ts @@ -104,7 +104,9 @@ export const CHART_CATEGORIES: ChartCategory[] = [ createChart('echarts', 'echarts-area', 'Area Chart', 'ECharts: Area', areaIcon), createChart('echarts', 'echarts-pie', 'Pie Chart', 'ECharts: Pie', pieIcon), createChart('echarts', 'echarts-heatmap', 'Heatmap', 'ECharts: Heatmap', heatmapIcon), + createChart('echarts', 'echarts-calendar', 'Calendar Heatmap *', 'ECharts: Calendar Heatmap *', heatmapIcon), createChart('echarts', 'echarts-histogram', 'Histogram', 'ECharts: Histogram', histogramIcon), + createChart('echarts', 'echarts-parallel', 'Parallel Coordinates *', 'ECharts: Parallel Coordinates *', lineIcon), createChart('echarts', 'echarts-boxplot', 'Boxplot', 'ECharts: Boxplot', boxplotIcon), createChart('echarts', 'echarts-radar', 'Radar Chart', 'ECharts: Radar', radarIcon), createChart('echarts', 'echarts-candlestick', 'Candlestick Chart', 'ECharts: Candlestick', candlestickIcon), @@ -123,6 +125,7 @@ export const CHART_CATEGORIES: ChartCategory[] = [ description: 'Practical Chart.js examples for familiar dashboard-style visuals.', charts: [ createChart('chartjs', 'chartjs-scatter', 'Scatter Plot', 'Chart.js: Scatter', scatterIcon), + 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-stacked-bar', 'Stacked Bar Chart', 'Chart.js: Stacked Bar', stackedBarIcon), From f2f68cca3429e0f3f8bc7bdb997abb92fc3ac294 Mon Sep 17 00:00:00 2001 From: Copilot Date: Fri, 12 Jun 2026 17:02:40 +0000 Subject: [PATCH 2/3] fix(echarts): clear calendar visualMap overlap + add dedicated icons - Calendar Heatmap: in the gallery the continuous visualMap has calculable on, so it also draws value labels and drag handles above the colour bar. The reserved bottom band (46px) was too small and the labels rode up into the last weekday row. Widen the band to 70px so the legend sits cleanly below the calendar. - Add purpose-built sidebar icons for the new gallery items instead of borrowing existing ones: Bubble (sized scatter circles), Calendar Heatmap (intensity day-grid) and Parallel Coordinates (crossed axes). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/echarts/templates/calendar.ts | 6 +++- .../assets/chart-icons/chart-icon-bubble.svg | 14 +++++++++ .../chart-icons/chart-icon-calendar.svg | 31 +++++++++++++++++++ .../chart-icons/chart-icon-parallel.svg | 12 +++++++ site/src/shared/chart-categories.ts | 9 ++++-- 5 files changed, 68 insertions(+), 4 deletions(-) create mode 100644 site/src/assets/chart-icons/chart-icon-bubble.svg create mode 100644 site/src/assets/chart-icons/chart-icon-calendar.svg create mode 100644 site/src/assets/chart-icons/chart-icon-parallel.svg diff --git a/packages/flint-js/src/echarts/templates/calendar.ts b/packages/flint-js/src/echarts/templates/calendar.ts index bc1b2279..4a955c68 100644 --- a/packages/flint-js/src/echarts/templates/calendar.ts +++ b/packages/flint-js/src/echarts/templates/calendar.ts @@ -97,7 +97,11 @@ export const ecCalendarHeatmapDef: ChartTemplateDef = { const calLeft = 44; // room for weekday labels const calRight = 16; const calTop = 34; // room for the month-label row - const vmHeight = 46; // visualMap bar + gap at the bottom + // Bottom band for the continuous visualMap. In the gallery `calculable` + // is on, so the colour bar also draws value labels + drag handles above + // it; reserve enough height that none of it rides up into the last + // weekday row of the calendar. + const vmHeight = 70; const gridH = 7 * cell; const canvasW = calLeft + weeks * cell + calRight; diff --git a/site/src/assets/chart-icons/chart-icon-bubble.svg b/site/src/assets/chart-icons/chart-icon-bubble.svg new file mode 100644 index 00000000..0b16762f --- /dev/null +++ b/site/src/assets/chart-icons/chart-icon-bubble.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/site/src/assets/chart-icons/chart-icon-calendar.svg b/site/src/assets/chart-icons/chart-icon-calendar.svg new file mode 100644 index 00000000..6b8d6634 --- /dev/null +++ b/site/src/assets/chart-icons/chart-icon-calendar.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/site/src/assets/chart-icons/chart-icon-parallel.svg b/site/src/assets/chart-icons/chart-icon-parallel.svg new file mode 100644 index 00000000..aaad272c --- /dev/null +++ b/site/src/assets/chart-icons/chart-icon-parallel.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/site/src/shared/chart-categories.ts b/site/src/shared/chart-categories.ts index d11de170..a226c05b 100644 --- a/site/src/shared/chart-categories.ts +++ b/site/src/shared/chart-categories.ts @@ -27,6 +27,9 @@ import treemapIcon from '../assets/chart-icons/chart-icon-treemap.svg'; import sunburstIcon from '../assets/chart-icons/chart-icon-sunburst.svg'; import sankeyIcon from '../assets/chart-icons/chart-icon-sankey.svg'; import rangedDotPlotIcon from '../assets/chart-icons/chart-icon-dot-plot-horizontal.svg'; +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'; export interface ChartEntry { id: string; @@ -104,9 +107,9 @@ export const CHART_CATEGORIES: ChartCategory[] = [ createChart('echarts', 'echarts-area', 'Area Chart', 'ECharts: Area', areaIcon), createChart('echarts', 'echarts-pie', 'Pie Chart', 'ECharts: Pie', pieIcon), createChart('echarts', 'echarts-heatmap', 'Heatmap', 'ECharts: Heatmap', heatmapIcon), - createChart('echarts', 'echarts-calendar', 'Calendar Heatmap *', 'ECharts: Calendar Heatmap *', heatmapIcon), + createChart('echarts', 'echarts-calendar', 'Calendar Heatmap *', 'ECharts: Calendar Heatmap *', calendarIcon), createChart('echarts', 'echarts-histogram', 'Histogram', 'ECharts: Histogram', histogramIcon), - createChart('echarts', 'echarts-parallel', 'Parallel Coordinates *', 'ECharts: Parallel Coordinates *', lineIcon), + createChart('echarts', 'echarts-parallel', 'Parallel Coordinates *', 'ECharts: Parallel Coordinates *', parallelIcon), createChart('echarts', 'echarts-boxplot', 'Boxplot', 'ECharts: Boxplot', boxplotIcon), createChart('echarts', 'echarts-radar', 'Radar Chart', 'ECharts: Radar', radarIcon), createChart('echarts', 'echarts-candlestick', 'Candlestick Chart', 'ECharts: Candlestick', candlestickIcon), @@ -125,7 +128,7 @@ export const CHART_CATEGORIES: ChartCategory[] = [ description: 'Practical Chart.js examples for familiar dashboard-style visuals.', charts: [ createChart('chartjs', 'chartjs-scatter', 'Scatter Plot', 'Chart.js: Scatter', scatterIcon), - createChart('chartjs', 'chartjs-bubble', 'Bubble Chart *', 'Chart.js: Bubble *', scatterIcon), + 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-stacked-bar', 'Stacked Bar Chart', 'Chart.js: Stacked Bar', stackedBarIcon), From 405fe7ede29371b4087d9fb9cd2bf76915a4c6e8 Mon Sep 17 00:00:00 2001 From: Copilot Date: Fri, 12 Jun 2026 17:36:35 +0000 Subject: [PATCH 3/3] fix(echarts,chartjs): correct parallel axis ranges and bubble edge clipping Issues found while grading rendered gallery output: - Parallel Coordinates: ECharts derives each parallelAxis range from only the first series, so with grouped data (e.g. Cars by Origin) any group whose values exceed the first group's max overflowed above the top axis label (USA MPG max 30 clipped Japan's 37.7). Compute each axis range across all rows and set explicit nice min/max so every line stays within the labelled domain and each axis uses its full height. - Bubble: large bubbles sitting at the data extremes were clipped by the plot edge, and the forced zero baseline compressed all points into the upper half. Scale to the data extent with 10% grace padding on both axes (dropping the forced beginAtZero) so nothing clips and bubbles spread out. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../flint-js/src/chartjs/templates/bubble.ts | 14 ++---- .../src/echarts/templates/parallel.ts | 47 ++++++++++++++++--- 2 files changed, 45 insertions(+), 16 deletions(-) diff --git a/packages/flint-js/src/chartjs/templates/bubble.ts b/packages/flint-js/src/chartjs/templates/bubble.ts index 2a3e7c97..2d7a64bd 100644 --- a/packages/flint-js/src/chartjs/templates/bubble.ts +++ b/packages/flint-js/src/chartjs/templates/bubble.ts @@ -91,8 +91,11 @@ export const cjsBubbleChartDef: ChartTemplateDef = { responsive: true, maintainAspectRatio: false, scales: { - x: { type: 'linear', title: { display: true, text: xField } }, - y: { type: 'linear', title: { display: true, text: yField } }, + // Bubble charts scale to the data extent (not a zero + // baseline) and pad both ends with `grace` so large bubbles + // sitting at the min/max aren't clipped by the plot edge. + x: { type: 'linear', grace: '10%', title: { display: true, text: xField } }, + y: { type: 'linear', grace: '10%', title: { display: true, text: yField } }, }, plugins: { tooltip: { enabled: true }, @@ -100,13 +103,6 @@ export const cjsBubbleChartDef: ChartTemplateDef = { }, }; - if (channelSemantics.x?.zero) { - config.options.scales.x.beginAtZero = channelSemantics.x.zero.zero !== false; - } - if (channelSemantics.y?.zero) { - config.options.scales.y.beginAtZero = channelSemantics.y.zero.zero !== false; - } - if (colorField) { const groups = new Map(); for (const row of table) { diff --git a/packages/flint-js/src/echarts/templates/parallel.ts b/packages/flint-js/src/echarts/templates/parallel.ts index e2952237..7c6f4086 100644 --- a/packages/flint-js/src/echarts/templates/parallel.ts +++ b/packages/flint-js/src/echarts/templates/parallel.ts @@ -36,6 +36,23 @@ function isNumericField(table: any[], field: string): boolean { return total > 0 && numeric / total >= 0.9; } +/** + * Round a [min, max] data extent outward to tidy bounds (multiples of a + * 1/2/5×10ⁿ step) so axis labels are clean and always contain the data. + */ +function niceBounds(min: number, max: number): [number, number] | null { + if (!isFinite(min) || !isFinite(max)) return null; + if (min === max) { + const pad = Math.abs(min) > 1e-9 ? Math.abs(min) * 0.1 : 1; + return [min - pad, max + pad]; + } + const rawStep = (max - min) / 5; + const mag = Math.pow(10, Math.floor(Math.log10(rawStep))); + const norm = rawStep / mag; + const step = (norm < 1.5 ? 1 : norm < 3 ? 2 : norm < 7 ? 5 : 10) * mag; + return [Math.floor(min / step) * step, Math.ceil(max / step) * step]; +} + export const ecParallelCoordinatesDef: ChartTemplateDef = { chart: 'Parallel Coordinates', template: { mark: 'line', encoding: {} }, @@ -61,13 +78,29 @@ export const ecParallelCoordinatesDef: ChartTemplateDef = { const palette = colorField ? getChartJsPalette(ctx, 'color') : DEFAULT_COLORS; const colors = palette.length > 0 ? palette : DEFAULT_COLORS; - const parallelAxis = dims.map((name, i) => ({ - dim: i, - name, - nameTextStyle: { fontSize: 11 }, - nameGap: 8, - axisLabel: { fontSize: 10 }, - })); + const parallelAxis = dims.map((name, i) => { + // Derive each axis range from every row. ECharts otherwise infers a + // parallelAxis extent from only the first series, which clips lines + // from later groups above the top label. + let lo = Infinity; + let hi = -Infinity; + for (const row of table) { + const v = Number(row[name]); + if (isFinite(v)) { + if (v < lo) lo = v; + if (v > hi) hi = v; + } + } + const bounds = niceBounds(lo, hi); + return { + dim: i, + name, + nameTextStyle: { fontSize: 11 }, + nameGap: 8, + axisLabel: { fontSize: 10 }, + ...(bounds ? { min: bounds[0], max: bounds[1] } : {}), + }; + }); const toLine = (row: any) => dims.map((d) => { const v = Number(row[d]);