From bc7754fa499ddf79c72a202cdda6b768d81762b7 Mon Sep 17 00:00:00 2001 From: "vitalii.semianchuk" Date: Mon, 27 Jul 2026 17:57:41 +0100 Subject: [PATCH 001/164] fix: use Math.max for pyramid chart right-bar palette fallback --- packages/flint-js/src/echarts/instantiate-spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/flint-js/src/echarts/instantiate-spec.ts b/packages/flint-js/src/echarts/instantiate-spec.ts index 3d588e63..8e6896cc 100644 --- a/packages/flint-js/src/echarts/instantiate-spec.ts +++ b/packages/flint-js/src/echarts/instantiate-spec.ts @@ -1098,7 +1098,7 @@ export function ecApplyLayoutToSpec( // ECharts default palette: first = blue (#5470c6), fourth = red (#ee6666) — not adjacent greens. const pal = effectivePalette && effectivePalette.length > 0 ? effectivePalette : DEFAULT_COLORS; const cLeft = pal[0]; - const cRight = pal.length > 3 ? pal[3] : pal[Math.min(1, pal.length - 1)]; + const cRight = pal.length > 3 ? pal[3] : pal[Math.max(0, pal.length - 1)]; let barIdx = 0; for (const s of option.series) { if (!s || s.type !== 'bar') continue; From 5999d650aaf8bd4c0afb7a657c629975314eeaea Mon Sep 17 00:00:00 2001 From: Chenglong Wang Date: Mon, 27 Jul 2026 13:30:48 -0700 Subject: [PATCH 002/164] minor fix --- packages/flint-js/src/echarts/instantiate-spec.ts | 2 +- site/src/shared/chart-categories.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/flint-js/src/echarts/instantiate-spec.ts b/packages/flint-js/src/echarts/instantiate-spec.ts index 8e6896cc..6329c0d3 100644 --- a/packages/flint-js/src/echarts/instantiate-spec.ts +++ b/packages/flint-js/src/echarts/instantiate-spec.ts @@ -300,7 +300,7 @@ function placePyramidChannelHeaders(option: any): void { const gw = Math.max(0, cw - gl - gr); const centerX = gl + gw / 2; const dx = gw / 4; - const topY = Math.max(4, gt - 10); + const topY = Math.max(4, gt - 24); const L = estimatePyramidYCategoryInsetPx(option, gw); const innerW = Math.max(gw - L, 1); diff --git a/site/src/shared/chart-categories.ts b/site/src/shared/chart-categories.ts index 27f2407f..43336c5d 100644 --- a/site/src/shared/chart-categories.ts +++ b/site/src/shared/chart-categories.ts @@ -189,6 +189,7 @@ const option = assembleECharts(input);`, createChart('echarts', 'echarts-bar', 'Bar Chart', 'ECharts: Bar', barIcon), createChart('echarts', 'echarts-stacked-bar', 'Stacked Bar Chart', 'ECharts: Stacked Bar', stackedBarIcon), createChart('echarts', 'echarts-grouped-bar', 'Grouped Bar Chart', 'ECharts: Grouped Bar', groupedBarIcon), + createChart('echarts', 'echarts-pyramid', 'Pyramid Chart', 'Pyramid Chart', pyramidIcon), createChart('echarts', 'echarts-area', 'Area Chart', 'ECharts: Area', areaIcon), createChart('echarts', 'echarts-range-area', 'Range Area Chart', 'ECharts: Range Area', rangeAreaIcon), createChart('echarts', 'echarts-pie', 'Pie Chart', 'ECharts: Pie', pieIcon), From 5671899949a6ad305fdeb6b0cdeebff21d4ec8fe Mon Sep 17 00:00:00 2001 From: Chenglong Wang Date: Mon, 27 Jul 2026 16:39:52 -0700 Subject: [PATCH 003/164] fixes for excel --- agent-skills/flint-chart-author/SKILL.md | 3 + docs/api-reference.md | 17 +++ packages/flint-js/src/chartjs/assemble.ts | 13 ++ packages/flint-js/src/echarts/assemble.ts | 20 +++ packages/flint-js/src/excel/artifact.ts | 2 +- packages/flint-js/src/excel/assemble.ts | 41 +++++-- packages/flint-js/src/excel/codegen.ts | 45 ++++--- packages/flint-js/src/excel/date-axis.ts | 43 +++++++ packages/flint-js/src/excel/runtime.ts | 54 +++++---- .../src/excel/templates/candlestick.ts | 14 +-- .../flint-js/src/excel/templates/funnel.ts | 2 +- .../flint-js/src/excel/templates/histogram.ts | 30 ++++- packages/flint-js/src/excel/types.ts | 12 ++ packages/flint-js/src/excel/typography.ts | 5 + packages/flint-js/src/plotly/assemble.ts | 24 ++++ packages/flint-js/tests/excel-codegen.test.ts | 20 +++ packages/flint-js/tests/excel-runtime.test.ts | 56 ++++++++- packages/flint-js/tests/smoke.test.ts | 114 ++++++++++++++++-- .../assets/flint-chart-author.SKILL.md | 3 + .../excel/office-runner/officejs/icon-16.png | Bin 198 -> 574 bytes .../excel/office-runner/officejs/icon-32.png | Bin 222 -> 1071 bytes .../excel/office-runner/officejs/icon-80.png | Bin 454 -> 2526 bytes .../excel/office-runner/officejs/icon.svg | 15 +-- .../excel/office-runner/officejs/manifest.xml | 14 +-- .../office-runner/officejs/taskpane.html | 2 +- 25 files changed, 452 insertions(+), 97 deletions(-) create mode 100644 packages/flint-js/src/excel/date-axis.ts create mode 100644 packages/flint-js/src/excel/typography.ts diff --git a/agent-skills/flint-chart-author/SKILL.md b/agent-skills/flint-chart-author/SKILL.md index 3b3fbedc..52d1adc1 100644 --- a/agent-skills/flint-chart-author/SKILL.md +++ b/agent-skills/flint-chart-author/SKILL.md @@ -92,6 +92,7 @@ interface ChartAssemblyInput { chartProperties?: Record; // per-chart tuning (optional) }; options?: Record; // global layout options (rarely needed) + field_display_names?: Record; // field → readable axis/legend title } ``` @@ -395,6 +396,8 @@ default: - **Sort a category axis by its measure:** `encodings.x = { field: "name", sortBy: "y", sortOrder: "descending" }`. - **Pick a color scheme:** `encodings.color = { field: "region", scheme: "tableau10" }`. - **Override an inferred type:** `encodings.x = { field: "year", type: "ordinal" }` (e.g. treat a year as discrete bands). +- **Use readable field titles:** `field_display_names = { percentageOfCountries: "Percentage of countries" }`. + Keep encodings bound to the real column name; Flint uses the display name for axis titles and legend headers. - **Resize the chart:** Flint sizes from two numbers — `baseSize` (the *target* it aims for, default 400×320) and `canvasSize` (a *hard ceiling* it may never exceed). With dense data the chart stretches from base toward the ceiling. diff --git a/docs/api-reference.md b/docs/api-reference.md index 3ac68882..60f6a78f 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -137,6 +137,23 @@ interface ChartAssemblyInput { Maps column name → semantic type. This drives encoding type, formatting, aggregation defaults, color class, and layout. See [Semantic Type](/documentation/semantic-types). +### `field_display_names` + +Maps raw column names to readable presentation labels used for axis titles and +legend headers. Keep encodings bound to the original field names: + +```ts +{ + field_display_names: { + percentageOfCountries: 'Percentage of countries' + }, + chart_spec: { + chartType: 'Bar Chart', + encodings: { x: 'country', y: 'percentageOfCountries' } + } +} +``` + ### `chart_spec` | Field | Description | diff --git a/packages/flint-js/src/chartjs/assemble.ts b/packages/flint-js/src/chartjs/assemble.ts index 53563145..14b12948 100644 --- a/packages/flint-js/src/chartjs/assemble.ts +++ b/packages/flint-js/src/chartjs/assemble.ts @@ -62,6 +62,17 @@ import { normalizeChartProperties } from '../core/normalize-properties'; * * @returns A Chart.js config object with optional `_warnings` and `_width`/`_height` hints */ +function applyFieldDisplayNames(config: any, names: Record | undefined): void { + if (!names) return; + const displayName = (value: unknown) => typeof value === 'string' ? names[value] ?? value : value; + for (const scale of Object.values(config.options?.scales ?? {}) as any[]) { + if (scale?.title?.text) scale.title.text = displayName(scale.title.text); + } + for (const dataset of config.data?.datasets ?? []) { + if (dataset?.label) dataset.label = displayName(dataset.label); + } +} + export function assembleChartjs(input: ChartAssemblyInput): any { const chartType = input.chart_spec.chartType; const semanticTypes = input.semantic_types ?? {}; @@ -444,6 +455,8 @@ export function assembleChartjs(input: ChartAssemblyInput): any { cjsConfig._pivot = legacyPivot.surface; } + applyFieldDisplayNames(cjsConfig, input.field_display_names); + return cjsConfig; } diff --git a/packages/flint-js/src/echarts/assemble.ts b/packages/flint-js/src/echarts/assemble.ts index 638c081c..64e36bc4 100644 --- a/packages/flint-js/src/echarts/assemble.ts +++ b/packages/flint-js/src/echarts/assemble.ts @@ -94,6 +94,24 @@ import { normalizeChartProperties } from '../core/normalize-properties'; * * @returns An ECharts option object with optional `_warnings` and `_width`/`_height` hints */ +function applyFieldDisplayNames(option: any, names: Record | undefined): void { + if (!names) return; + const displayName = (value: unknown) => typeof value === 'string' ? names[value] ?? value : value; + for (const axisKey of ['xAxis', 'yAxis', 'singleAxis']) { + const axes = Array.isArray(option[axisKey]) ? option[axisKey] : [option[axisKey]]; + for (const axis of axes) { + if (axis?.name) axis.name = displayName(axis.name); + } + } + for (const series of option.series ?? []) { + if (series?.name) series.name = displayName(series.name); + } + const graphics = Array.isArray(option.graphic) ? option.graphic : option.graphic ? [option.graphic] : []; + for (const graphic of graphics) { + if (graphic?.style?.text) graphic.style.text = displayName(graphic.style.text); + } +} + export function assembleECharts(input: ChartAssemblyInput): any { const chartType = input.chart_spec.chartType; const semanticTypes = input.semantic_types ?? {}; @@ -534,6 +552,8 @@ export function assembleECharts(input: ChartAssemblyInput): any { // Clean internal-only props delete ecOption._legendWidth; + applyFieldDisplayNames(ecOption, input.field_display_names); + return ecOption; } diff --git a/packages/flint-js/src/excel/artifact.ts b/packages/flint-js/src/excel/artifact.ts index 728cf0ed..cb6ed958 100644 --- a/packages/flint-js/src/excel/artifact.ts +++ b/packages/flint-js/src/excel/artifact.ts @@ -95,7 +95,7 @@ export function prepareExcelArtifact(value: unknown): PreparedExcelArtifact { rangeA1: `A1:${excelColumnLetter(columns - 1)}${rows}`, chartType, numericAxis: /XYScatter|Bubble/i.test(chartType), - dateAxis: /Stock/i.test(chartType), + dateAxis: /Stock/i.test(chartType) || spec.categoryAxis?.categoryType === 'DateAxis', hasAxes: !/Pie|Doughnut|Treemap|Sunburst|Funnel/i.test(chartType), isBar: /Bar|Column/i.test(chartType), isLine: /Line/i.test(chartType), diff --git a/packages/flint-js/src/excel/assemble.ts b/packages/flint-js/src/excel/assemble.ts index de1b538d..0b26b049 100644 --- a/packages/flint-js/src/excel/assemble.ts +++ b/packages/flint-js/src/excel/assemble.ts @@ -38,6 +38,7 @@ import type { ExcelNativeSeriesSpec, ExcelSeriesBy, } from './types'; +import { excelDateAxis, excelDateSerial } from './date-axis'; type Cell = string | number | null; @@ -84,11 +85,6 @@ function focusedNumericAxis(values: number[]): Partial labelBudget ? Math.ceil(categoryCount / labelBudget) : undefined; -} - /** Normalize shorthand (`"x": "field"`) to `{ field }`. */ function normalizeEncodings( raw: Record, @@ -101,6 +97,22 @@ function normalizeEncodings( return out; } +function applyFieldDisplayNames( + spec: ExcelChartSpec, + fieldDisplayNames: Record | undefined, +): ExcelChartSpec { + if (!fieldDisplayNames) return spec; + const displayName = (value: string | number | null) => + typeof value === 'string' ? fieldDisplayNames[value] ?? value : value; + if (spec.categoryAxis?.title) spec.categoryAxis.title = String(displayName(spec.categoryAxis.title)); + if (spec.valueAxis?.title) spec.valueAxis.title = String(displayName(spec.valueAxis.title)); + if (spec.data.length > 0) spec.data[0] = spec.data[0].map(displayName); + if (spec.series) { + for (const series of spec.series) series.name = String(displayName(series.name)); + } + return spec; +} + /** Distinct values of a field, first-seen order. */ function distinct(rows: any[], field: string): Cell[] { const seen = new Set(); @@ -204,7 +216,10 @@ export function assembleExcel(input: ChartAssemblyInput): ExcelChartSpec { } if (chartTemplate.instantiate) { - return chartTemplate.instantiate(templateContext); + return applyFieldDisplayNames( + chartTemplate.instantiate(templateContext), + input.field_display_names, + ); } if (flintType === 'Bar Chart' || flintType === 'Grouped Bar Chart' || flintType === 'Stacked Bar Chart') { @@ -486,7 +501,9 @@ export function assembleExcel(input: ChartAssemblyInput): ExcelChartSpec { data.push([catField, ...seriesDescriptors.map((descriptor) => descriptor.label)]); for (let categoryIndex = 0; categoryIndex < categories.length; categoryIndex += 1) { data.push([ - String(categories[categoryIndex]), + typeOf(catCh!) === 'temporal' + ? excelDateSerial(categories[categoryIndex]) + : String(categories[categoryIndex]), ...seriesValues.map((values) => values[categoryIndex]), ]); } @@ -544,11 +561,11 @@ export function assembleExcel(input: ChartAssemblyInput): ExcelChartSpec { : {}; spec.categoryAxis = { title: catField, + ...(orientation === 'vertical' && typeOf(catCh!) === 'temporal' + ? excelDateAxis(convertedData.map((row) => row[catField]), base.width) + : {}), labelFontSize: orientation === 'horizontal' && data.length > 25 - ? Math.max(5, Math.min(10, ((base.height - 80) / (data.length - 1)) * 0.72)) - : undefined, - tickLabelSpacing: orientation === 'vertical' && typeOf(catCh!) === 'temporal' - ? temporalLabelSpacing(data.length - 1, base.width) + ? Math.max(8, Math.min(13, ((base.height - 80) / (data.length - 1)) * 0.9)) : undefined, reversePlotOrder: orientation === 'horizontal' && typeOf(catCh!) !== 'temporal', ...numericXScale, @@ -572,5 +589,5 @@ export function assembleExcel(input: ChartAssemblyInput): ExcelChartSpec { spec.overlap = 0; } - return spec; + return applyFieldDisplayNames(spec, input.field_display_names); } diff --git a/packages/flint-js/src/excel/codegen.ts b/packages/flint-js/src/excel/codegen.ts index 099f549c..30a59a03 100644 --- a/packages/flint-js/src/excel/codegen.ts +++ b/packages/flint-js/src/excel/codegen.ts @@ -1,5 +1,12 @@ import { prepareExcelArtifact } from './artifact'; import type { ExcelAxisSpec, ExcelNativeChartSpec } from './types'; +import { + EXCEL_AXIS_TITLE_FONT_SIZE, + EXCEL_CHART_TITLE_FONT_SIZE, + EXCEL_DATA_LABEL_FONT_SIZE, + EXCEL_LABEL_FONT_SIZE, + EXCEL_LEGEND_FONT_SIZE, +} from './typography'; export interface OfficeJsCodegenOptions { scale?: number; @@ -21,12 +28,16 @@ export interface GeneratedOfficeJs { function axisCode(axisName: 'categoryAxis' | 'valueAxis', axis: ExcelAxisSpec): string[] { const target = `chart.axes.${axisName}`; const lines: string[] = []; + if (axis.categoryType) lines.push(` ${target}.categoryType = ${JSON.stringify(axis.categoryType)};`); + if (axis.baseTimeUnit) lines.push(` ${target}.baseTimeUnit = ${JSON.stringify(axis.baseTimeUnit)};`); + if (axis.majorTimeUnitScale) lines.push(` ${target}.majorTimeUnitScale = ${JSON.stringify(axis.majorTimeUnitScale)};`); if (axis.title) { lines.push(` ${target}.title.text = ${JSON.stringify(axis.title)};`); lines.push(` ${target}.title.visible = true;`); + lines.push(` ${target}.title.format.font.size = ${EXCEL_AXIS_TITLE_FONT_SIZE};`); } if (axis.numberFormat) lines.push(` ${target}.numberFormat = ${JSON.stringify(axis.numberFormat)};`); - if (axis.labelFontSize !== undefined) lines.push(` ${target}.format.font.size = ${axis.labelFontSize};`); + lines.push(` ${target}.format.font.size = ${axis.labelFontSize ?? EXCEL_LABEL_FONT_SIZE};`); if (axis.tickLabelSpacing !== undefined) lines.push(` ${target}.tickLabelSpacing = ${axis.tickLabelSpacing};`); if (axis.reversePlotOrder !== undefined) lines.push(` ${target}.reversePlotOrder = ${axis.reversePlotOrder};`); if (axis.minimumScale !== undefined) lines.push(` ${target}.minimum = ${axis.minimumScale};`); @@ -79,42 +90,46 @@ export function generateOfficeJs(value: unknown, options: OfficeJsCodegenOptions ); if (spec.series?.length) { - lines.push(" chart.series.load('items');", ' await context.sync();'); - lines.push(` if (chart.series.items.length < ${spec.series.length}) throw new Error('Excel inferred too few series.');`); + lines.push( + " chart.series.load('items');", + ' await context.sync();', + ' chart.series.items.forEach((series) => series.delete());', + ' await context.sync();', + ); spec.series.forEach((binding, index) => { + const columnCount = binding.columnCount ?? 1; lines.push( - ` chart.series.items[${index}].name = ${JSON.stringify(binding.name)};`, - ` chart.series.items[${index}].setXAxisValues(sheet.getRangeByIndexes(1, ${binding.xColumn}, ${binding.rowCount}, 1));`, - ` chart.series.items[${index}].setValues(sheet.getRangeByIndexes(1, ${binding.yColumn}, ${binding.rowCount}, 1));`, + ` const boundSeries${index} = chart.series.add(${JSON.stringify(binding.name)}, ${index});`, + ` boundSeries${index}.setXAxisValues(sheet.getRangeByIndexes(${binding.xRow ?? 1}, ${binding.xColumn}, ${binding.rowCount}, ${columnCount}));`, + ` boundSeries${index}.setValues(sheet.getRangeByIndexes(${binding.yRow ?? 1}, ${binding.yColumn}, ${binding.rowCount}, ${columnCount}));`, ); if (binding.bubbleSizeColumn !== undefined) { - lines.push(` chart.series.items[${index}].setBubbleSizes(sheet.getRangeByIndexes(1, ${binding.bubbleSizeColumn}, ${binding.rowCount}, 1));`); + lines.push(` boundSeries${index}.setBubbleSizes(sheet.getRangeByIndexes(1, ${binding.bubbleSizeColumn}, ${binding.rowCount}, 1));`); } }); - lines.push( - ` for (let index = chart.series.items.length - 1; index >= ${spec.series.length}; index -= 1) {`, - ' chart.series.getItemAt(index).delete();', - ' await context.sync();', - ' }', - ); } lines.push(` chart.width = ${width};`, ` chart.height = ${height};`); if (spec.title) { - lines.push(` chart.title.text = ${JSON.stringify(spec.title)};`, ' chart.title.visible = true;'); + lines.push( + ` chart.title.text = ${JSON.stringify(spec.title)};`, + ' chart.title.visible = true;', + ` chart.title.format.font.size = ${EXCEL_CHART_TITLE_FONT_SIZE};`, + ); } if (spec.legend) { lines.push(` chart.legend.visible = ${spec.legend.visible};`); if (spec.legend.visible && spec.legend.position) { lines.push(` chart.legend.position = ${JSON.stringify(spec.legend.position)};`); } + if (spec.legend.visible) lines.push(` chart.legend.format.font.size = ${EXCEL_LEGEND_FONT_SIZE};`); } if (spec.dataLabels) { lines.push(` chart.dataLabels.visible = ${spec.dataLabels.visible};`); if (spec.dataLabels.position) lines.push(` chart.dataLabels.position = ${JSON.stringify(spec.dataLabels.position)};`); if (spec.dataLabels.numberFormat) lines.push(` chart.dataLabels.numberFormat = ${JSON.stringify(spec.dataLabels.numberFormat)};`); if (spec.dataLabels.fontColor) lines.push(` chart.dataLabels.format.font.color = ${JSON.stringify(spec.dataLabels.fontColor)};`); - if (spec.dataLabels.fontSize !== undefined) lines.push(` chart.dataLabels.format.font.size = ${spec.dataLabels.fontSize};`); + lines.push(` chart.dataLabels.format.font.size = ${spec.dataLabels.fontSize ?? EXCEL_DATA_LABEL_FONT_SIZE};`); } if (prepared.hasAxes) { if (spec.categoryAxis) lines.push(...axisCode('categoryAxis', spec.categoryAxis)); diff --git a/packages/flint-js/src/excel/date-axis.ts b/packages/flint-js/src/excel/date-axis.ts new file mode 100644 index 00000000..3b4d3336 --- /dev/null +++ b/packages/flint-js/src/excel/date-axis.ts @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import type { ExcelAxisSpec } from './types'; + +const EXCEL_EPOCH = Date.UTC(1899, 11, 30); +const DAY_MILLISECONDS = 24 * 60 * 60 * 1000; + +export function excelDateSerial(value: unknown): number { + return (new Date(value as string | number | Date).getTime() - EXCEL_EPOCH) / DAY_MILLISECONDS; +} + +function niceInterval(value: number): number { + const power = 10 ** Math.floor(Math.log10(value)); + const fraction = value / power; + return (fraction <= 1 ? 1 : fraction <= 2 ? 2 : fraction <= 5 ? 5 : 10) * power; +} + +export function excelDateAxis(values: unknown[], width: number): Pick< + ExcelAxisSpec, + 'categoryType' | 'baseTimeUnit' | 'majorUnit' | 'majorTimeUnitScale' | 'numberFormat' +> { + const axis: ExcelAxisSpec = { + categoryType: 'DateAxis', + baseTimeUnit: 'Days', + numberFormat: 'yyyy-mm-dd', + }; + const labelBudget = Math.max(2, Math.floor((width - 90) / 75)); + if (values.length <= labelBudget) return axis; + + const times = values + .map((value) => new Date(value as string | number | Date).getTime()) + .filter(Number.isFinite); + if (times.length < 2) return axis; + const roughDays = Math.max(1, Math.max(...times) - Math.min(...times)) / DAY_MILLISECONDS / labelBudget; + if (roughDays >= 365) { + return { ...axis, majorUnit: niceInterval(roughDays / 365), majorTimeUnitScale: 'Years' }; + } + if (roughDays >= 28) { + return { ...axis, majorUnit: niceInterval(roughDays / (365 / 12)), majorTimeUnitScale: 'Months' }; + } + return { ...axis, majorUnit: niceInterval(roughDays), majorTimeUnitScale: 'Days' }; +} diff --git a/packages/flint-js/src/excel/runtime.ts b/packages/flint-js/src/excel/runtime.ts index 82603ca2..7dfefaca 100644 --- a/packages/flint-js/src/excel/runtime.ts +++ b/packages/flint-js/src/excel/runtime.ts @@ -1,4 +1,11 @@ import { prepareExcelArtifact, type PreparedExcelArtifact } from './artifact'; +import { + EXCEL_AXIS_TITLE_FONT_SIZE, + EXCEL_CHART_TITLE_FONT_SIZE, + EXCEL_DATA_LABEL_FONT_SIZE, + EXCEL_LABEL_FONT_SIZE, + EXCEL_LEGEND_FONT_SIZE, +} from './typography'; export interface OfficeJsExcelApi { run(callback: (context: any) => Promise): Promise; @@ -16,11 +23,12 @@ export interface ExcelRenderResult { inspection: unknown | null; } -function applyChartFormat(chart: any, prepared: PreparedExcelArtifact): void { +function applyChartFormat(chart: any, prepared: PreparedExcelArtifact, nativeSeriesCount: number): void { const { spec } = prepared; if (spec.legend) { chart.legend.visible = spec.legend.visible; if (spec.legend.visible && spec.legend.position) chart.legend.position = spec.legend.position; + if (spec.legend.visible) chart.legend.format.font.size = EXCEL_LEGEND_FONT_SIZE; } if (prepared.hasAxes) { const axes = [ @@ -29,12 +37,16 @@ function applyChartFormat(chart: any, prepared: PreparedExcelArtifact): void { ] as const; for (const [axis, format] of axes) { if (!format) continue; + if (format.categoryType) axis.categoryType = format.categoryType; + if (format.baseTimeUnit) axis.baseTimeUnit = format.baseTimeUnit; + if (format.majorTimeUnitScale) axis.majorTimeUnitScale = format.majorTimeUnitScale; if (format.title) { axis.title.text = format.title; axis.title.visible = true; + axis.title.format.font.size = EXCEL_AXIS_TITLE_FONT_SIZE; } if (format.numberFormat) axis.numberFormat = format.numberFormat; - if (format.labelFontSize !== undefined) axis.format.font.size = format.labelFontSize; + axis.format.font.size = format.labelFontSize ?? EXCEL_LABEL_FONT_SIZE; if (format.tickLabelSpacing !== undefined) axis.tickLabelSpacing = format.tickLabelSpacing; if (format.reversePlotOrder !== undefined) axis.reversePlotOrder = format.reversePlotOrder; if (format.minimumScale !== undefined) axis.minimum = format.minimumScale; @@ -47,9 +59,9 @@ function applyChartFormat(chart: any, prepared: PreparedExcelArtifact): void { if (spec.dataLabels.position) chart.dataLabels.position = spec.dataLabels.position; if (spec.dataLabels.numberFormat) chart.dataLabels.numberFormat = spec.dataLabels.numberFormat; if (spec.dataLabels.fontColor) chart.dataLabels.format.font.color = spec.dataLabels.fontColor; - if (spec.dataLabels.fontSize !== undefined) chart.dataLabels.format.font.size = spec.dataLabels.fontSize; + chart.dataLabels.format.font.size = spec.dataLabels.fontSize ?? EXCEL_DATA_LABEL_FONT_SIZE; } - for (let index = 0; index < prepared.seriesCount; index += 1) { + for (let index = 0; index < Math.min(prepared.seriesCount, nativeSeriesCount); index += 1) { const series = chart.series.getItemAt(index); const format = spec.seriesFormats?.[index]; if (format?.color) { @@ -77,15 +89,14 @@ async function inspectChart(context: any, sheet: any, dataRange: any, chart: any for (const series of chart.series.items) { series.binOptions.load('type,count,width,allowOverflow,overflowValue,allowUnderflow,underflowValue'); } - const primaryCategoryAxis = chart.axes.getItemOrNullObject('Category', 'Primary'); - const primaryValueAxis = chart.axes.getItemOrNullObject('Value', 'Primary'); - const secondaryValueAxis = chart.axes.getItemOrNullObject('Value', 'Secondary'); - for (const axis of [primaryCategoryAxis, primaryValueAxis, secondaryValueAxis]) { - axis.load('isNullObject,axisType,axisGroup,visible,minimum,maximum,numberFormat'); + const primaryCategoryAxis = chart.axes.categoryAxis; + const primaryValueAxis = chart.axes.valueAxis; + for (const axis of [primaryCategoryAxis, primaryValueAxis]) { + axis.load('axisType,axisGroup,visible,minimum,maximum,numberFormat'); axis.title.load('text,visible'); } await context.sync(); - const describeAxis = (axis: any) => axis.isNullObject ? null : { ...axis.toJSON(), title: axis.title.toJSON() }; + const describeAxis = (axis: any) => ({ ...axis.toJSON(), title: axis.title.toJSON() }); return { sourceRange: { address: dataRange.address, @@ -106,7 +117,6 @@ async function inspectChart(context: any, sheet: any, dataRange: any, chart: any axes: { primaryCategory: describeAxis(primaryCategoryAxis), primaryValue: describeAxis(primaryValueAxis), - secondaryValue: describeAxis(secondaryValueAxis), }, }, }; @@ -153,22 +163,17 @@ export async function renderExcelChart( if (spec.series?.length) { chart.series.load('items'); await context.sync(); - if (chart.series.items.length < spec.series.length) { - throw new Error(`Excel inferred ${chart.series.items.length} series; ${spec.series.length} required.`); - } + chart.series.items.forEach((series: any) => series.delete()); + await context.sync(); for (const [index, binding] of spec.series.entries()) { - const series = chart.series.items[index]; - series.name = binding.name; - series.setXAxisValues(sheet.getRangeByIndexes(1, binding.xColumn, binding.rowCount, 1)); - series.setValues(sheet.getRangeByIndexes(1, binding.yColumn, binding.rowCount, 1)); + const series = chart.series.add(binding.name, index); + const columnCount = binding.columnCount ?? 1; + series.setXAxisValues(sheet.getRangeByIndexes(binding.xRow ?? 1, binding.xColumn, binding.rowCount, columnCount)); + series.setValues(sheet.getRangeByIndexes(binding.yRow ?? 1, binding.yColumn, binding.rowCount, columnCount)); if (binding.bubbleSizeColumn !== undefined) { series.setBubbleSizes(sheet.getRangeByIndexes(1, binding.bubbleSizeColumn, binding.rowCount, 1)); } } - for (let index = chart.series.items.length - 1; index >= spec.series.length; index -= 1) { - chart.series.getItemAt(index).delete(); - await context.sync(); - } } const width = spec.width ?? 400; @@ -178,8 +183,11 @@ export async function renderExcelChart( if (spec.title) { chart.title.text = spec.title; chart.title.visible = true; + chart.title.format.font.size = EXCEL_CHART_TITLE_FONT_SIZE; } - applyChartFormat(chart, prepared); + chart.series.load?.('items'); + await context.sync(); + applyChartFormat(chart, prepared, chart.series.items?.length ?? prepared.seriesCount); await context.sync(); const image = chart.getImage( Math.round(width * (96 / 72) * scale), diff --git a/packages/flint-js/src/excel/templates/candlestick.ts b/packages/flint-js/src/excel/templates/candlestick.ts index fada1375..9d8e67a4 100644 --- a/packages/flint-js/src/excel/templates/candlestick.ts +++ b/packages/flint-js/src/excel/templates/candlestick.ts @@ -2,15 +2,10 @@ // Licensed under the MIT License. import { formatSpecToExcel } from '../chart-types'; +import { excelDateAxis, excelDateSerial } from '../date-axis'; import type { ExcelTemplateDef } from './types'; const PRICE_CHANNELS = ['open', 'high', 'low', 'close'] as const; -const EXCEL_EPOCH = Date.UTC(1899, 11, 30); -const DAY_MILLISECONDS = 24 * 60 * 60 * 1000; - -function excelDateSerial(value: unknown): number { - return (new Date(value as string | number | Date).getTime() - EXCEL_EPOCH) / DAY_MILLISECONDS; -} function niceStep(span: number): number { const rough = span / 5; @@ -66,8 +61,6 @@ export const excelCandlestickDef: ExcelTemplateDef = { const xField = fieldOf('x')!; const fields = PRICE_CHANNELS.map((channel) => fieldOf(channel)!); const base = input.chart_spec.baseSize ?? { width: 480, height: 320 }; - const labelBudget = Math.max(12, Math.floor((base.width - 90) / 14)); - const tickLabelSpacing = table.length > labelBudget ? Math.ceil(table.length / labelBudget) : undefined; const lows = table.map((row) => Number(row[fields[2]])); const highs = table.map((row) => Number(row[fields[1]])); const minimum = Math.min(...lows); @@ -89,7 +82,10 @@ export const excelCandlestickDef: ExcelTemplateDef = { ...fields.map((field) => Number(row[field])), ]), ], - categoryAxis: { title: xField, numberFormat: 'yyyy-mm-dd', tickLabelSpacing }, + categoryAxis: { + title: xField, + ...excelDateAxis(table.map((row) => row[xField]), base.width), + }, valueAxis: { title: 'Price', numberFormat: formatSpecToExcel(semantics.close?.format), diff --git a/packages/flint-js/src/excel/templates/funnel.ts b/packages/flint-js/src/excel/templates/funnel.ts index 401037de..34cabd5e 100644 --- a/packages/flint-js/src/excel/templates/funnel.ts +++ b/packages/flint-js/src/excel/templates/funnel.ts @@ -51,7 +51,7 @@ export const excelFunnelChartDef: ExcelTemplateDef = { visible: true, numberFormat: formatSpecToExcel(semantics.size?.format), fontColor: '#FFFFFF', - fontSize: 11, + fontSize: 13, }, seriesFormats: [{ color: '#4472C4' }], width: base.width, diff --git a/packages/flint-js/src/excel/templates/histogram.ts b/packages/flint-js/src/excel/templates/histogram.ts index bf6964eb..632acb8f 100644 --- a/packages/flint-js/src/excel/templates/histogram.ts +++ b/packages/flint-js/src/excel/templates/histogram.ts @@ -57,20 +57,38 @@ export const excelHistogramDef: ExcelTemplateDef = { if (seriesIndex >= 0) seriesCounts[seriesIndex][binIndex] += 1; } const base = input.chart_spec.baseSize ?? { width: 480, height: 320 }; + const data = colorField + ? [ + [valueField, ...labels], + ...seriesKeys.map((name, seriesIndex) => [name, ...seriesCounts[seriesIndex]]), + ] + : [ + [valueField, ...seriesKeys], + ...labels.map((label, index) => [label, seriesCounts[0][index]]), + ]; + const series = colorField + ? seriesKeys.map((name, index) => ({ + name, + xRow: 0, + xColumn: 1, + yRow: index + 1, + yColumn: 1, + rowCount: 1, + columnCount: binCount, + })) + : undefined; return { schema: 'flint.excel.chart/v1', kind: 'chart', chartType: colorField ? 'ColumnStacked' : 'ColumnClustered', title: `Distribution of ${valueField}`, - seriesBy: 'Columns', - data: [ - [valueField, ...seriesKeys], - ...labels.map((label, index) => [label, ...seriesCounts.map((series) => series[index])]), - ], + seriesBy: colorField ? 'Rows' : 'Columns', + series, + data, categoryAxis: { title: valueField }, valueAxis: { title: 'Count', numberFormat: '0' }, legend: { visible: Boolean(colorField), position: 'Bottom' }, - gapWidth: 0, + gapWidth: 20, width: base.width, height: base.height, warnings: [], diff --git a/packages/flint-js/src/excel/types.ts b/packages/flint-js/src/excel/types.ts index 8d236eb5..c44e035f 100644 --- a/packages/flint-js/src/excel/types.ts +++ b/packages/flint-js/src/excel/types.ts @@ -24,6 +24,12 @@ export type ExcelLegendPosition = 'Top' | 'Bottom' | 'Left' | 'Right'; export interface ExcelAxisSpec { /** Axis title text (from the field display name). */ title?: string; + /** Native Excel category-axis interpretation. */ + categoryType?: 'DateAxis' | 'TextAxis'; + /** Base unit used by a native Excel date axis. */ + baseTimeUnit?: 'Days' | 'Months' | 'Years'; + /** Unit associated with `majorUnit` on a native Excel date axis. */ + majorTimeUnitScale?: 'Days' | 'Months' | 'Years'; /** Axis-label font size in points, used when dense categories need explicit sizing. */ labelFontSize?: number; /** Number of categories between displayed labels; underlying data stays intact. */ @@ -62,6 +68,12 @@ export interface ExcelNativeSeriesSpec { yColumn: number; /** Number of populated worksheet rows in this series. */ rowCount: number; + /** Optional zero-based worksheet row containing X values. Defaults to the first data row. */ + xRow?: number; + /** Optional zero-based worksheet row containing Y values. Defaults to the first data row. */ + yRow?: number; + /** Number of populated worksheet columns for a horizontal series. Defaults to one. */ + columnCount?: number; /** Optional zero-based worksheet column containing bubble sizes. */ bubbleSizeColumn?: number; } diff --git a/packages/flint-js/src/excel/typography.ts b/packages/flint-js/src/excel/typography.ts new file mode 100644 index 00000000..314cb72f --- /dev/null +++ b/packages/flint-js/src/excel/typography.ts @@ -0,0 +1,5 @@ +export const EXCEL_CHART_TITLE_FONT_SIZE = 18; +export const EXCEL_AXIS_TITLE_FONT_SIZE = 15; +export const EXCEL_LABEL_FONT_SIZE = 13; +export const EXCEL_LEGEND_FONT_SIZE = 13; +export const EXCEL_DATA_LABEL_FONT_SIZE = 13; \ No newline at end of file diff --git a/packages/flint-js/src/plotly/assemble.ts b/packages/flint-js/src/plotly/assemble.ts index 0f18d537..1a1c804a 100644 --- a/packages/flint-js/src/plotly/assemble.ts +++ b/packages/flint-js/src/plotly/assemble.ts @@ -65,6 +65,28 @@ import { normalizeChartProperties } from '../core/normalize-properties'; * * @returns A Plotly figure with optional `_warnings` and `_width`/`_height` hints */ +function applyFieldDisplayNames(figure: any, names: Record | undefined): void { + if (!names) return; + const displayName = (value: unknown) => typeof value === 'string' ? names[value] ?? value : value; + for (const [key, axis] of Object.entries(figure.layout ?? {}) as Array<[string, any]>) { + if (/^[xy]axis\d*$/.test(key) && axis?.title?.text) { + axis.title.text = displayName(axis.title.text); + } + } + if (figure.layout?.legend?.title?.text) { + figure.layout.legend.title.text = displayName(figure.layout.legend.title.text); + } + for (const trace of figure.data ?? []) { + if (trace?.name) trace.name = displayName(trace.name); + if (trace?.marker?.colorbar?.title?.text) { + trace.marker.colorbar.title.text = displayName(trace.marker.colorbar.title.text); + } + if (trace?.colorbar?.title?.text) { + trace.colorbar.title.text = displayName(trace.colorbar.title.text); + } + } +} + export function assemblePlotly(input: ChartAssemblyInput): any { const chartType = input.chart_spec.chartType; const semanticTypes = input.semantic_types ?? {}; @@ -416,6 +438,8 @@ export function assemblePlotly(input: ChartAssemblyInput): any { figure._pivot = legacyPivot.surface; } + applyFieldDisplayNames(figure, input.field_display_names); + return figure; } diff --git a/packages/flint-js/tests/excel-codegen.test.ts b/packages/flint-js/tests/excel-codegen.test.ts index 36206977..bb4ecdb2 100644 --- a/packages/flint-js/tests/excel-codegen.test.ts +++ b/packages/flint-js/tests/excel-codegen.test.ts @@ -48,6 +48,7 @@ describe('Excel Office.js artifacts', () => { }); expect(generated.code).toContain('sheet.charts.add("Funnel", dataRange, "Columns")'); expect(generated.code).toContain('chart.dataLabels.format.font.color = "#FFFFFF"'); + expect(generated.code).toContain('chart.title.format.font.size = 18'); expect(generated.code).toContain('chart.series.getItemAt(index)'); expect(generated.code).toContain('chart.getImage(1280, 853, Excel.ImageFittingMode.fit)'); expect(() => new Function('Excel', `${generated.code}\nreturn main;`)).not.toThrow(); @@ -58,4 +59,23 @@ describe('Excel Office.js artifacts', () => { expect(funnelArtifact).not.toHaveProperty('scale'); expect(funnelArtifact).not.toHaveProperty('cleanWorksheet'); }); + + it('generates deterministic explicit series bindings', () => { + const generated = generateOfficeJs({ + ...funnelArtifact, + chartType: 'ColumnStacked', + data: [['Bin', 'Male', 'Female'], ['150-160', 7, 40]], + series: [ + { name: 'Male', xRow: 0, xColumn: 1, yRow: 1, yColumn: 1, rowCount: 1, columnCount: 2 }, + { name: 'Female', xRow: 0, xColumn: 1, yRow: 2, yColumn: 1, rowCount: 1, columnCount: 2 }, + ], + }); + + expect(generated.code).toContain('chart.series.items.forEach((series) => series.delete())'); + expect(generated.code).toContain('chart.series.add("Male", 0)'); + expect(generated.code).toContain('chart.series.add("Female", 1)'); + expect(generated.code).toContain('boundSeries1.setXAxisValues(sheet.getRangeByIndexes(0, 1, 1, 2))'); + expect(generated.code).toContain('boundSeries1.setValues(sheet.getRangeByIndexes(2, 1, 1, 2))'); + expect(() => new Function('Excel', `${generated.code}\nreturn main;`)).not.toThrow(); + }); }); diff --git a/packages/flint-js/tests/excel-runtime.test.ts b/packages/flint-js/tests/excel-runtime.test.ts index 70b4efb4..92f88f09 100644 --- a/packages/flint-js/tests/excel-runtime.test.ts +++ b/packages/flint-js/tests/excel-runtime.test.ts @@ -11,20 +11,40 @@ function createMockExcel() { numberFormat: null as unknown, image: null as unknown, labels: {} as Record, + typography: {} as Record, fillColor: '', + deletedSeries: 0, + addedSeries: [] as Array<{ name: string | undefined; index: number | undefined }>, + xBindings: [] as unknown[], + valueBindings: [] as unknown[], clears: 0, syncs: 0, }; const series = { + delete: () => { calls.deletedSeries += 1; }, + setXAxisValues: (value: unknown) => { calls.xBindings.push(value); }, + setValues: (value: unknown) => { calls.valueBindings.push(value); }, format: { fill: { setSolidColor: (color: string) => { calls.fillColor = color; } }, line: {}, }, }; const chart = { - series: { getItemAt: () => series }, - title: {}, - legend: {}, + series: { + items: [series], + load: vi.fn(), + add: (name?: string, index?: number) => { + calls.addedSeries.push({ name, index }); + return series; + }, + getItemAt: () => series, + }, + title: { format: { font: { + set size(value: number) { calls.typography.chartTitle = value; }, + } } }, + legend: { format: { font: { + set size(value: number) { calls.typography.legend = value; }, + } } }, dataLabels: { format: { font: {} }, set visible(value: boolean) { calls.labels.visible = value; }, @@ -63,6 +83,8 @@ function createMockExcel() { calls.rangeAddress = address; return range; }, + getRangeByIndexes: (row: number, column: number, rowCount: number, columnCount: number) => + ({ row, column, rowCount, columnCount }), }; const run = vi.fn(async (callback: (context: unknown) => Promise) => callback({ workbook: { worksheets: { getActiveWorksheet: () => sheet } }, @@ -109,6 +131,7 @@ describe('renderExcelChart', () => { fontSize: 11, }); expect(calls.fillColor).toBe('#4472C4'); + expect(calls.typography).toEqual({ chartTitle: 18 }); expect(calls.image).toEqual({ width: 1280, height: 853, mode: 'fit' }); expect(calls.clears).toBe(1); expect(result).toEqual({ pngBase64: 'iVBORw0KGgoMOCK', inspection: null }); @@ -121,4 +144,31 @@ describe('renderExcelChart', () => { ); expect(run).not.toHaveBeenCalled(); }); + + it('rebuilds explicitly bound series instead of relying on Excel inference', async () => { + const { excel, calls } = createMockExcel(); + await renderExcelChart(excel, { + ...artifact, + chartType: 'ColumnStacked', + data: [['Bin', 'Male', 'Female'], ['150-160', 7, 40]], + series: [ + { name: 'Male', xRow: 0, xColumn: 1, yRow: 1, yColumn: 1, rowCount: 1, columnCount: 2 }, + { name: 'Female', xRow: 0, xColumn: 1, yRow: 2, yColumn: 1, rowCount: 1, columnCount: 2 }, + ], + }); + + expect(calls.deletedSeries).toBe(1); + expect(calls.addedSeries).toEqual([ + { name: 'Male', index: 0 }, + { name: 'Female', index: 1 }, + ]); + expect(calls.xBindings).toEqual([ + { row: 0, column: 1, rowCount: 1, columnCount: 2 }, + { row: 0, column: 1, rowCount: 1, columnCount: 2 }, + ]); + expect(calls.valueBindings).toEqual([ + { row: 1, column: 1, rowCount: 1, columnCount: 2 }, + { row: 2, column: 1, rowCount: 1, columnCount: 2 }, + ]); + }); }); diff --git a/packages/flint-js/tests/smoke.test.ts b/packages/flint-js/tests/smoke.test.ts index 4e7977b8..25526e40 100644 --- a/packages/flint-js/tests/smoke.test.ts +++ b/packages/flint-js/tests/smoke.test.ts @@ -49,6 +49,36 @@ describe('public API smoke', () => { expect(config.type ?? config.data ?? config.options).toBeDefined(); }); + it('uses field display names across native backend axis and series titles', () => { + const input = { + data: { values: [ + { country: 'France', percentageOfCountries: 42 }, + { country: 'Japan', percentageOfCountries: 47 }, + ] }, + semantic_types: { country: 'Country', percentageOfCountries: 'Percent' }, + field_display_names: { + country: 'Country', + percentageOfCountries: 'Percentage of countries', + }, + chart_spec: { + chartType: 'Line Chart', + encodings: { x: 'country', y: 'percentageOfCountries' }, + }, + }; + + const echarts = assembleECharts(input) as any; + expect(echarts.xAxis.name).toBe('Country'); + expect(echarts.yAxis.name).toBe('Percentage of countries'); + + const chartjs = assembleChartjs(input) as any; + expect(chartjs.options.scales.x.title.text).toBe('Country'); + expect(chartjs.options.scales.y.title.text).toBe('Percentage of countries'); + + const plotly = assemblePlotly(input) as any; + expect(plotly.layout.xaxis.title.text).toBe('Country'); + expect(plotly.layout.yaxis.title.text).toBe('Percentage of countries'); + }); + it('assembleExcel returns an Excel chart spec with a wide data matrix', () => { const spec = assembleExcel(INPUT) as any; expect(spec).toBeDefined(); @@ -68,6 +98,29 @@ describe('public API smoke', () => { expect(spec.seriesBy).toBe('Columns'); }); + it('assembleExcel uses field display names for native axis titles', () => { + const spec = assembleExcel({ + data: { values: [ + { year: '2024', percentageOfCountries: 42 }, + { year: '2025', percentageOfCountries: 47 }, + ] }, + semantic_types: { year: 'Category', percentageOfCountries: 'Percent' }, + field_display_names: { + year: 'Year', + percentageOfCountries: 'Percentage of countries', + }, + chart_spec: { + chartType: 'Line Chart', + encodings: { x: 'year', y: 'percentageOfCountries' }, + }, + }); + + expect(spec.categoryAxis?.title).toBe('Year'); + expect(spec.valueAxis?.title).toBe('Percentage of countries'); + expect(spec.data[0]).toEqual(['Year', 'Percentage of countries']); + expect(spec.data[1]).toEqual(['2024', 42]); + }); + it('assembleExcel preserves bar-template roles when both axes are quantitative', () => { const spec = assembleExcel({ data: { values: [{ X: 1, Value: 10 }, { X: 2, Value: 20 }] }, @@ -104,7 +157,31 @@ describe('public API smoke', () => { expect(spec.chartType).toBe('ColumnClustered'); expect(spec.data[0]).toEqual(['Value', 'Count']); expect(spec.data.slice(1).reduce((sum: number, row: any[]) => sum + row[1], 0)).toBe(3); - expect(spec.gapWidth).toBe(0); + expect(spec.gapWidth).toBe(20); + }); + + it('assembleExcel emits grouped histograms as native row series', () => { + const spec = assembleExcel({ + data: { values: [ + { Height: 160, Gender: 'Male' }, + { Height: 170, Gender: 'Female' }, + ] }, + semantic_types: { Height: 'Quantity', Gender: 'Category' }, + chart_spec: { chartType: 'Histogram', encodings: { x: 'Height', color: 'Gender' } }, + }); + + expect(spec.chartType).toBe('ColumnStacked'); + expect(spec.seriesBy).toBe('Rows'); + expect(spec.series).toEqual([ + { name: 'Male', xRow: 0, xColumn: 1, yRow: 1, yColumn: 1, rowCount: 1, columnCount: 5 }, + { name: 'Female', xRow: 0, xColumn: 1, yRow: 2, yColumn: 1, rowCount: 1, columnCount: 5 }, + ]); + expect(spec.data).toEqual([ + ['Height', '160-162', '162-164', '164-166', '166-168', '168-170'], + ['Male', 1, 0, 0, 0, 0], + ['Female', 0, 0, 0, 0, 1], + ]); + expect(spec.gapWidth).toBe(20); }); it('assembleExcel emits a native delta Waterfall with connector lines', () => { @@ -337,7 +414,7 @@ describe('public API smoke', () => { expect(spec.data.slice(1).map((row: any[]) => row[0])).toEqual([46024, 46027]); }); - it('assembleExcel thins dense Candlestick date labels without dropping OHLC rows', () => { + it('assembleExcel uses native date intervals for dense Candlestick labels without dropping OHLC rows', () => { const values = Array.from({ length: 90 }, (_, index) => ({ Date: new Date(Date.UTC(2026, 0, index + 1)).toISOString().slice(0, 10), Open: 100 + index, @@ -356,7 +433,13 @@ describe('public API smoke', () => { }) as any; expect(spec.data).toHaveLength(91); - expect(spec.categoryAxis.tickLabelSpacing).toBe(3); + expect(spec.categoryAxis).toMatchObject({ + categoryType: 'DateAxis', + baseTimeUnit: 'Days', + majorUnit: 20, + majorTimeUnitScale: 'Days', + }); + expect(spec.categoryAxis.tickLabelSpacing).toBeUndefined(); }); it('assembleExcel rejects color-grouped native Boxplots', () => { @@ -427,7 +510,14 @@ describe('public API smoke', () => { }) as any; expect(spec.data).toHaveLength(values.length + 1); - expect(spec.categoryAxis.tickLabelSpacing).toBe(2); + expect(spec.data[1][0]).toBe(46023); + expect(spec.categoryAxis).toMatchObject({ + categoryType: 'DateAxis', + baseTimeUnit: 'Days', + majorUnit: 10, + majorTimeUnitScale: 'Days', + }); + expect(spec.categoryAxis.tickLabelSpacing).toBeUndefined(); }); it('assembleExcel emits grouped bubble series with explicit size ranges', () => { @@ -657,7 +747,7 @@ describe('public API smoke', () => { visible: true, numberFormat: undefined, fontColor: '#FFFFFF', - fontSize: 11, + fontSize: 13, }); expect(spec.seriesFormats).toEqual([{ color: '#4472C4' }]); }); @@ -827,7 +917,7 @@ describe('public API smoke', () => { expect(excel.data.slice(1).map((row: any[]) => row[0])).toEqual(expectedOrder); expect(expectedOrder).toEqual(values.slice(0, expectedOrder.length).map((row) => row.Group)); expect(excel.data.length).toBeLessThan(values.length + 1); - expect(excel.categoryAxis.labelFontSize).toBe(5); + expect(excel.categoryAxis.labelFontSize).toBe(8); }); it('assembleExcel caps dense bars using the selected value sort', () => { @@ -867,9 +957,9 @@ describe('public API smoke', () => { expect(spec.data).toEqual([ ['Date', 'A', 'B'], - ['2026-01-01', 10, null], - ['2026-01-02', 20, 20], - ['2026-01-03', 30, null], + [46023, 10, null], + [46024, 20, 20], + [46025, 30, null], ]); }); @@ -898,9 +988,9 @@ describe('public API smoke', () => { expect(spec.data).toEqual([ ['Date', 'actual', 'forecast'], - ['2026-01-01', 10, null], - ['2026-01-02', 12, 12], - ['2026-01-03', null, 15], + [46023, 10, null], + [46024, 12, 12], + [46025, null, 15], ]); expect(spec.seriesFormats).toEqual([ { color: '#4472C4', lineStyle: 'Continuous' }, diff --git a/packages/flint-mcp/assets/flint-chart-author.SKILL.md b/packages/flint-mcp/assets/flint-chart-author.SKILL.md index 3b3fbedc..52d1adc1 100644 --- a/packages/flint-mcp/assets/flint-chart-author.SKILL.md +++ b/packages/flint-mcp/assets/flint-chart-author.SKILL.md @@ -92,6 +92,7 @@ interface ChartAssemblyInput { chartProperties?: Record; // per-chart tuning (optional) }; options?: Record; // global layout options (rarely needed) + field_display_names?: Record; // field → readable axis/legend title } ``` @@ -395,6 +396,8 @@ default: - **Sort a category axis by its measure:** `encodings.x = { field: "name", sortBy: "y", sortOrder: "descending" }`. - **Pick a color scheme:** `encodings.color = { field: "region", scheme: "tableau10" }`. - **Override an inferred type:** `encodings.x = { field: "year", type: "ordinal" }` (e.g. treat a year as discrete bands). +- **Use readable field titles:** `field_display_names = { percentageOfCountries: "Percentage of countries" }`. + Keep encodings bound to the real column name; Flint uses the display name for axis titles and legend headers. - **Resize the chart:** Flint sizes from two numbers — `baseSize` (the *target* it aims for, default 400×320) and `canvasSize` (a *hard ceiling* it may never exceed). With dense data the chart stretches from base toward the ceiling. diff --git a/test-harness/excel/office-runner/officejs/icon-16.png b/test-harness/excel/office-runner/officejs/icon-16.png index 8665db7691e81764e96847913d67a721cf80d14f..caab692a405c92d0cf44526768102e1b996c29e0 100644 GIT binary patch delta 550 zcmV+>0@?k>0lox~BYy$~NklL}7`($tw|tT;2tcxI$S zL#iO+0~g+N?!6E99)^G9{|fq#jHE=F8+-QP-W4iC7Dh9Qi+@(3ulQ2^tzh5L6L$qc zJex|T*ivorc4_j?m^HcmR$S&)meUPik}PU^hai18KQ8dX^(eZj36VMkG$01+Ja*?Z{txtJU;QVcNaRn)(HrUb;hyTS1SosL}4Uq7F~8bSe0 z{h=sILm<92OjxD?YuTgQ?MsoVA=@Qlu3V-?sM^^mFJGKpTsYYLLLxE3B zk|8E~H|bg_9H37iNDt%(Btd@cc^-_#Dw<8VL%<9CZ1rP#H^nvrfsc|tSb*130i8!MyPwE8i22V;h1TKQsSs<_T@BONYQm$qa3@ ot*@8nXCG&-QTa#K75oi90iisv5m}m|?*IS*07*qoM6N<$g6L@lEdT%j delta 171 zcmV;c0960J1jYf7BYyymNkl#7Gg4J%YRcpcKs$f^w9pK(f()bcz*lM>}(j_VeSdseFDp`sDC4!6R~kBuqT1;-VSI%YI-|ne}8zZZy~y;CFnUmnBfIML9D{Q z{`L6erz+1D!n4Pe?hE?TXE5Sp%79vhGEds;sQO|e)HY)le3>aTQGdE0u?o|~)-rT@@S!gB%A`{pv@kzMMlOdQkbFTcHl0Ai^Z)obfzFB95wcNN z;HiP3JY!tV&r^wYT3uNtj^oH5+(!o^hlw#p8p>`*yY^e@@8ctC02d05#}c@dD1E_B zqusx~m7(PN8l{qs-yx6BN2nN50mgpPuFmvR6Mt$7JaO#gC9!bY*~w#bCH`F{J)5QU zMiL`sIG!hOn;-KQesV7YPxhbE?I&Il>&|s-GHZ=11uR^On`EW<)Upq>Ycr$Ngen2+ zMdNaNCJ-Im%A`w^))TPs`2#!EKn31L1;#MckSc*Az0pb8XQ7nBvpGVKy2U~Ot zdw-!8f+%7?v|&xkk6udO0E+1gA~c6%GC8c~ui^B&LX z8^2#(LboXltwcoa4^TFJj+l9lO!8x>CV!ha@$5@tfyd9375U>Ei+{Y2Zp%0XitLE! zsWC{rhHe#=0(fI4M4wM>^{(wOw7NvpqY={e(Ml1!0RBCjzPa@00OGD-XieD>$>zq0 z%oow6jYOOnv79U^udZlUE?{Wav?Nd@(*1Nlsxg{LCBwYO8{JLGD`9ANlzRgI0Tk;m Vd@u;XWYz!x002ovPDHLkV1gJB_ErD@ delta 195 zcmV;!06hP%2;KpZBYyy;Nkl57O!vI9#%6) zFd2CbQZ$VkXe-|{JfcazK10+0Pcixh*j_l}^B8^C9?m@ctY{O^=QYeoGaqIQ;{?ky zRWXdY6ku$?8M`Uq`+P6pJn$~uW(qJaKo(Ibw`fd&n>g?_qC5v^gIlBkV*-?4!si2m x65e4&5Mv7v4VH!5OaT<2O@J)l3wRsl0SjyJ%pog;c(MQh002ovPDHLkV1g_lPVfK# diff --git a/test-harness/excel/office-runner/officejs/icon-80.png b/test-harness/excel/office-runner/officejs/icon-80.png index c38979c4a5acc1dceabb2b98564162c140000307..4f606e42d13ac84d1f6c2d47d7f3ed8f4dddce92 100644 GIT binary patch delta 2517 zcmV;`2`cu+1KtymBYz2{NklpS^>rBm{g#w zVFYzhSi`gmx_wxxvJd+**tAXChlwyvo2p4$_%`jWsWh?hr9f9YX`4i6@ zHBgeOfs#}Wl%#5)Bvk_?sTwFr)j&zA1{yH1KiJjO7wRPgss9 ziw@uYZU5=x2Y+z8Yz*xW1~Q}_)-QJEXlx&!9(1{ z=Hut<5f%lnSsAR~y#3dz>V26p$E@FJ+0rHC@p_>iF@A6C+FxG({aQ#Nu==`4(_V!P z*X$l#gTG6XTZxqIZtDA27a72CEhy`s+P=@FD8F>M+;cu@#uzm>H$lzCjEV6#8_y7F z?F2NYeA_~JbZ>)Q|<7v0U{l`M-T8I@ZmVc9?C{V@}GI8Q`^YdBCbTN%O5#|$S zjCvgoZyD+D+3h1QEZUTD49p=s1bL;(^& zV|3C%&R+j>w@x864Z~Gdcul9O>d#80Yd)&trsXSEK#8$6aFf2i@f?&jG>yHD%;++^ zB7e(fAU`BDXZ9a1w^IBRFhFGg%V&u{L0enG+RnAt?t0vrrMOiR4TUm3++P2#TPG2k zrYr^`U2`p%Xu1}nv2hhSolYq8WHQs*z9-K9e9wlV$MFk1k6ZafkmkKEm(pu?#pQ$7 zw-27KdJBfoGES!h1~x)jhnpCSMPGUA8-MS3k)H=~>me0+3Ul+kUf(YurSW!^0oA}D+K@%BFtt!|je?Zsv~5fN-N z@H5+XXJz)hnaJ#ag_va8TzSc8%3bzeX7=}xV;MOk;T5Z?z$sgaFh3|K_Zbm3GJ&kBK;fv=C-z$Fx zOj`C;p6Z1j9u>pzMMc>7`7ffKw1-0Ddr^iy8pvG*VRSxx;am`E4g+5a27f8T$Nk!j zpRQ8(U(Ia?qeEsSbtlYW#-5ePE7FJ*nlp(kF2iu$b zLj7a{%LdxjeWX(<8jB9+j3{KFj>F-=eDJYciZjM?#j!8~C5%-p8-KWAbH@qQJe59bh+Kw4;Mih!00H7 z=tEFt!IFV8!_(B%B;AXRK+Pr*AJ3(=S$vq>xRv6yBHrSWQZ=9qrcID}_E9KxbtX{P zO{ddP=3ppqcq$h?P@J?k{O@xop~^y$|Ft+W-AAF+)v;Gx;`Q!j3Hc$!7S&wJfMsjD*HY{@q!!c!`mEmcr)YY-WKz^X`Rml71`><0u zflYtlW)L>eN`Lo}oAF`w8Ko|V|9pHj1|<(4h52e~p*ZC&VK~ADS{a`5eNW_IFf&LH z;o-1>mFG#^OoRy;I{)I1I>JOW9E!c6f1*- zVlc)u)vLZTbmKoiKv;H!3=|!n@?pTt8J{S28HnF;RPp)u3|=|^cZBmoNdrZ@4?ZZ+ z$Ma^HOeBi-$fBSh?!O(mybW?*VYo^XSohg3Jhks8q841XBhv??FP~OC%6EruTdDF5NTA0&^D|Zh+1#RRpZ|@bm{#F!UcihGDRTRUQem`Av=vUYal-; z+=}OSwsWpNYV}?lwuqY)%nCm1gF!PRNUT|2E!j5N4VfvHc%%Jd@4Zc zbT?Mx*AQAlbdZ2P_x(FJuRIA^F-RMzBV%MK1AlqSs$S2x7wVue!ZrhSaAF6Ev>wzo z&ma`S$Ut7(xoc~K3wcl&!Elv@!1|5tSdHDKj5O+l6_0xIcKGsM$cm+G2I|~5lCMZ= zML35-23FcZVMN&t)In-@6)MK_4dg=rr@yA7Qd2e`hHGBQH&Xvn(fxyB{)PZ{G4BTI z=uTbDR3cfIPN(L52w)crW*~6@ fHBgeOfs*_eeQy1Jx7mDL00000NkvXXu0mjf>M+a= literal 454 zcmeAS@N?(olHy`uVBq!ia0vp^0U*r51|<6gKdoh8VC?mDaSW-L^Y*5pS9YL;YvSAv z7VE8oEY@ptOSt$BT;sH4eV&*t+nz8r=)7(#Z}~|zbH&K`!nor48|%V5m#eRm*pPBy z5~DYp&clY)r)SUqc>ZEUecb}H7mGU?jlWzx(jfUOBk2H7t%+emo87D>4X>AP|9Gyt zVvp4OJ?r0}t(d;OaZsKOXa_wswJy*K~(Zh5!_pPIs7$1>J_ z3k_Lw#^UQcy$ady~1#?=(97bcCa}APm=?8eg(r>jM2+jJhQj>9jM_$U{a63@h zLPq179WDpDe}Dd%7yg-j2@A3WnmQQQo8?RRPO$eRHR7qG6{$+}}{x9K-mdAf!{4jXB`njxgN@xNA_2s&B diff --git a/test-harness/excel/office-runner/officejs/icon.svg b/test-harness/excel/office-runner/officejs/icon.svg index 49fe1007..f5e44f1b 100644 --- a/test-harness/excel/office-runner/officejs/icon.svg +++ b/test-harness/excel/office-runner/officejs/icon.svg @@ -1,8 +1,9 @@ - - - - - - - + + Flint + + + + + + diff --git a/test-harness/excel/office-runner/officejs/manifest.xml b/test-harness/excel/office-runner/officejs/manifest.xml index c933cb0e..b0985db4 100644 --- a/test-harness/excel/office-runner/officejs/manifest.xml +++ b/test-harness/excel/office-runner/officejs/manifest.xml @@ -19,8 +19,8 @@ en-US - - + + https://localhost:3000 @@ -29,8 +29,8 @@ - - + + @@ -74,9 +74,9 @@ - - - + + + diff --git a/test-harness/excel/office-runner/officejs/taskpane.html b/test-harness/excel/office-runner/officejs/taskpane.html index 447371c9..5daa9c1c 100644 --- a/test-harness/excel/office-runner/officejs/taskpane.html +++ b/test-harness/excel/office-runner/officejs/taskpane.html @@ -56,7 +56,7 @@
- Flint + Flint
Flint Render
Office.js chart → PNG worker
From 9ce2eb4f50ce0cc4a8d45027c8fdca8f9962bba1 Mon Sep 17 00:00:00 2001 From: Chenglong Wang Date: Mon, 27 Jul 2026 17:50:33 -0700 Subject: [PATCH 004/164] histogram group --- packages/flint-js/src/excel/codegen.ts | 9 ++++---- packages/flint-js/src/excel/runtime.ts | 11 +++++----- .../flint-js/src/excel/templates/histogram.ts | 21 +++++++------------ packages/flint-js/src/excel/types.ts | 6 ------ packages/flint-js/tests/excel-codegen.test.ts | 11 +++++----- packages/flint-js/tests/excel-runtime.test.ts | 12 +++++------ packages/flint-js/tests/smoke.test.ts | 11 ++++------ 7 files changed, 34 insertions(+), 47 deletions(-) diff --git a/packages/flint-js/src/excel/codegen.ts b/packages/flint-js/src/excel/codegen.ts index 30a59a03..0b8e0ec4 100644 --- a/packages/flint-js/src/excel/codegen.ts +++ b/packages/flint-js/src/excel/codegen.ts @@ -93,15 +93,16 @@ export function generateOfficeJs(value: unknown, options: OfficeJsCodegenOptions lines.push( " chart.series.load('items');", ' await context.sync();', - ' chart.series.items.forEach((series) => series.delete());', + ' for (let index = chart.series.items.length - 1; index >= 0; index -= 1) {', + ' chart.series.getItemAt(index).delete();', + ' }', ' await context.sync();', ); spec.series.forEach((binding, index) => { - const columnCount = binding.columnCount ?? 1; lines.push( ` const boundSeries${index} = chart.series.add(${JSON.stringify(binding.name)}, ${index});`, - ` boundSeries${index}.setXAxisValues(sheet.getRangeByIndexes(${binding.xRow ?? 1}, ${binding.xColumn}, ${binding.rowCount}, ${columnCount}));`, - ` boundSeries${index}.setValues(sheet.getRangeByIndexes(${binding.yRow ?? 1}, ${binding.yColumn}, ${binding.rowCount}, ${columnCount}));`, + ` boundSeries${index}.setXAxisValues(sheet.getRangeByIndexes(1, ${binding.xColumn}, ${binding.rowCount}, 1));`, + ` boundSeries${index}.setValues(sheet.getRangeByIndexes(1, ${binding.yColumn}, ${binding.rowCount}, 1));`, ); if (binding.bubbleSizeColumn !== undefined) { lines.push(` boundSeries${index}.setBubbleSizes(sheet.getRangeByIndexes(1, ${binding.bubbleSizeColumn}, ${binding.rowCount}, 1));`); diff --git a/packages/flint-js/src/excel/runtime.ts b/packages/flint-js/src/excel/runtime.ts index 7dfefaca..6b7e4ed2 100644 --- a/packages/flint-js/src/excel/runtime.ts +++ b/packages/flint-js/src/excel/runtime.ts @@ -163,13 +163,14 @@ export async function renderExcelChart( if (spec.series?.length) { chart.series.load('items'); await context.sync(); - chart.series.items.forEach((series: any) => series.delete()); + for (let index = chart.series.items.length - 1; index >= 0; index -= 1) { + chart.series.getItemAt(index).delete(); + } await context.sync(); for (const [index, binding] of spec.series.entries()) { - const series = chart.series.add(binding.name, index); - const columnCount = binding.columnCount ?? 1; - series.setXAxisValues(sheet.getRangeByIndexes(binding.xRow ?? 1, binding.xColumn, binding.rowCount, columnCount)); - series.setValues(sheet.getRangeByIndexes(binding.yRow ?? 1, binding.yColumn, binding.rowCount, columnCount)); + const series = chart.series.add(binding.name, index); + series.setXAxisValues(sheet.getRangeByIndexes(1, binding.xColumn, binding.rowCount, 1)); + series.setValues(sheet.getRangeByIndexes(1, binding.yColumn, binding.rowCount, 1)); if (binding.bubbleSizeColumn !== undefined) { series.setBubbleSizes(sheet.getRangeByIndexes(1, binding.bubbleSizeColumn, binding.rowCount, 1)); } diff --git a/packages/flint-js/src/excel/templates/histogram.ts b/packages/flint-js/src/excel/templates/histogram.ts index 632acb8f..8515ead2 100644 --- a/packages/flint-js/src/excel/templates/histogram.ts +++ b/packages/flint-js/src/excel/templates/histogram.ts @@ -59,31 +59,24 @@ export const excelHistogramDef: ExcelTemplateDef = { const base = input.chart_spec.baseSize ?? { width: 480, height: 320 }; const data = colorField ? [ - [valueField, ...labels], - ...seriesKeys.map((name, seriesIndex) => [name, ...seriesCounts[seriesIndex]]), + [valueField, labels[0], '', ...labels.slice(1)], + ...seriesKeys.map((name, seriesIndex) => [ + name, + seriesCounts[seriesIndex][0], + 0, + ...seriesCounts[seriesIndex].slice(1), + ]), ] : [ [valueField, ...seriesKeys], ...labels.map((label, index) => [label, seriesCounts[0][index]]), ]; - const series = colorField - ? seriesKeys.map((name, index) => ({ - name, - xRow: 0, - xColumn: 1, - yRow: index + 1, - yColumn: 1, - rowCount: 1, - columnCount: binCount, - })) - : undefined; return { schema: 'flint.excel.chart/v1', kind: 'chart', chartType: colorField ? 'ColumnStacked' : 'ColumnClustered', title: `Distribution of ${valueField}`, seriesBy: colorField ? 'Rows' : 'Columns', - series, data, categoryAxis: { title: valueField }, valueAxis: { title: 'Count', numberFormat: '0' }, diff --git a/packages/flint-js/src/excel/types.ts b/packages/flint-js/src/excel/types.ts index c44e035f..42ef4779 100644 --- a/packages/flint-js/src/excel/types.ts +++ b/packages/flint-js/src/excel/types.ts @@ -68,12 +68,6 @@ export interface ExcelNativeSeriesSpec { yColumn: number; /** Number of populated worksheet rows in this series. */ rowCount: number; - /** Optional zero-based worksheet row containing X values. Defaults to the first data row. */ - xRow?: number; - /** Optional zero-based worksheet row containing Y values. Defaults to the first data row. */ - yRow?: number; - /** Number of populated worksheet columns for a horizontal series. Defaults to one. */ - columnCount?: number; /** Optional zero-based worksheet column containing bubble sizes. */ bubbleSizeColumn?: number; } diff --git a/packages/flint-js/tests/excel-codegen.test.ts b/packages/flint-js/tests/excel-codegen.test.ts index bb4ecdb2..0d82ba87 100644 --- a/packages/flint-js/tests/excel-codegen.test.ts +++ b/packages/flint-js/tests/excel-codegen.test.ts @@ -66,16 +66,17 @@ describe('Excel Office.js artifacts', () => { chartType: 'ColumnStacked', data: [['Bin', 'Male', 'Female'], ['150-160', 7, 40]], series: [ - { name: 'Male', xRow: 0, xColumn: 1, yRow: 1, yColumn: 1, rowCount: 1, columnCount: 2 }, - { name: 'Female', xRow: 0, xColumn: 1, yRow: 2, yColumn: 1, rowCount: 1, columnCount: 2 }, + { name: 'Male', xColumn: 0, yColumn: 1, rowCount: 1 }, + { name: 'Female', xColumn: 0, yColumn: 2, rowCount: 1 }, ], }); - expect(generated.code).toContain('chart.series.items.forEach((series) => series.delete())'); + expect(generated.code).toContain('for (let index = chart.series.items.length - 1; index >= 0; index -= 1)'); + expect(generated.code).toContain('chart.series.getItemAt(index).delete()'); expect(generated.code).toContain('chart.series.add("Male", 0)'); expect(generated.code).toContain('chart.series.add("Female", 1)'); - expect(generated.code).toContain('boundSeries1.setXAxisValues(sheet.getRangeByIndexes(0, 1, 1, 2))'); - expect(generated.code).toContain('boundSeries1.setValues(sheet.getRangeByIndexes(2, 1, 1, 2))'); + expect(generated.code).toContain('boundSeries1.setXAxisValues(sheet.getRangeByIndexes(1, 0, 1, 1))'); + expect(generated.code).toContain('boundSeries1.setValues(sheet.getRangeByIndexes(1, 2, 1, 1))'); expect(() => new Function('Excel', `${generated.code}\nreturn main;`)).not.toThrow(); }); }); diff --git a/packages/flint-js/tests/excel-runtime.test.ts b/packages/flint-js/tests/excel-runtime.test.ts index 92f88f09..746a40d0 100644 --- a/packages/flint-js/tests/excel-runtime.test.ts +++ b/packages/flint-js/tests/excel-runtime.test.ts @@ -152,8 +152,8 @@ describe('renderExcelChart', () => { chartType: 'ColumnStacked', data: [['Bin', 'Male', 'Female'], ['150-160', 7, 40]], series: [ - { name: 'Male', xRow: 0, xColumn: 1, yRow: 1, yColumn: 1, rowCount: 1, columnCount: 2 }, - { name: 'Female', xRow: 0, xColumn: 1, yRow: 2, yColumn: 1, rowCount: 1, columnCount: 2 }, + { name: 'Male', xColumn: 0, yColumn: 1, rowCount: 1 }, + { name: 'Female', xColumn: 0, yColumn: 2, rowCount: 1 }, ], }); @@ -163,12 +163,12 @@ describe('renderExcelChart', () => { { name: 'Female', index: 1 }, ]); expect(calls.xBindings).toEqual([ - { row: 0, column: 1, rowCount: 1, columnCount: 2 }, - { row: 0, column: 1, rowCount: 1, columnCount: 2 }, + { row: 1, column: 0, rowCount: 1, columnCount: 1 }, + { row: 1, column: 0, rowCount: 1, columnCount: 1 }, ]); expect(calls.valueBindings).toEqual([ - { row: 1, column: 1, rowCount: 1, columnCount: 2 }, - { row: 2, column: 1, rowCount: 1, columnCount: 2 }, + { row: 1, column: 1, rowCount: 1, columnCount: 1 }, + { row: 1, column: 2, rowCount: 1, columnCount: 1 }, ]); }); }); diff --git a/packages/flint-js/tests/smoke.test.ts b/packages/flint-js/tests/smoke.test.ts index 25526e40..0b0bdb5f 100644 --- a/packages/flint-js/tests/smoke.test.ts +++ b/packages/flint-js/tests/smoke.test.ts @@ -172,14 +172,11 @@ describe('public API smoke', () => { expect(spec.chartType).toBe('ColumnStacked'); expect(spec.seriesBy).toBe('Rows'); - expect(spec.series).toEqual([ - { name: 'Male', xRow: 0, xColumn: 1, yRow: 1, yColumn: 1, rowCount: 1, columnCount: 5 }, - { name: 'Female', xRow: 0, xColumn: 1, yRow: 2, yColumn: 1, rowCount: 1, columnCount: 5 }, - ]); + expect(spec.series).toBeUndefined(); expect(spec.data).toEqual([ - ['Height', '160-162', '162-164', '164-166', '166-168', '168-170'], - ['Male', 1, 0, 0, 0, 0], - ['Female', 0, 0, 0, 0, 1], + ['Height', '160-162', '', '162-164', '164-166', '166-168', '168-170'], + ['Male', 1, 0, 0, 0, 0, 0], + ['Female', 0, 0, 0, 0, 0, 1], ]); expect(spec.gapWidth).toBe(20); }); From cd77a810c16e19b10c0131b095d6fd29a131f6b5 Mon Sep 17 00:00:00 2001 From: nyxst4ck <289980115+nyxst4ck@users.noreply.github.com> Date: Tue, 28 Jul 2026 09:52:25 -0300 Subject: [PATCH 005/164] docs: quote pip extras install examples --- packages/flint-py/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/flint-py/README.md b/packages/flint-py/README.md index 14a44dbb..02e2446a 100644 --- a/packages/flint-py/README.md +++ b/packages/flint-py/README.md @@ -58,6 +58,6 @@ and asserts deep equality against the recorded JS spec. ```bash # Run the Python compatibility tests cd packages/flint-py -uv pip install -e .[test] +uv pip install -e '.[test]' uv run pytest -q ``` From dc8984ffcef4ca389371374ac8fc76cbd1055857 Mon Sep 17 00:00:00 2001 From: Chenglong Wang Date: Tue, 28 Jul 2026 09:26:04 -0700 Subject: [PATCH 006/164] cleanup --- docs/DEVELOPMENT.md | 4 +- docs/adding-a-backend.md | 5 + docs/test_plan.md | 33 +- .../antv-g6-example-editor-three-column.png | Bin 115848 -> 0 bytes .../antv-g6-gallery-grid.png | Bin 69282 -> 0 bytes .../echarts-example-editor-option-preview.png | Bin 43428 -> 0 bytes ...echarts-examples-line-category-gallery.png | Bin 93983 -> 0 bytes .../observable-home-hero-collage.png | Bin 112100 -> 0 bytes ...vable-plot-gallery-line-moving-average.png | Bin 51299 -> 0 bytes .../vega-lite-example-gallery-index.png | Bin 65358 -> 0 bytes .../vega-lite-simple-bar-chart-example.png | Bin 57146 -> 0 bytes docs/website-design-plan.md | 320 --------------- packages/flint-js/src/docs/test_plan.md | 369 ------------------ site/src/shared/docs-catalog.ts | 6 + 14 files changed, 41 insertions(+), 696 deletions(-) delete mode 100644 docs/website-design-assets/antv-g6-example-editor-three-column.png delete mode 100644 docs/website-design-assets/antv-g6-gallery-grid.png delete mode 100644 docs/website-design-assets/echarts-example-editor-option-preview.png delete mode 100644 docs/website-design-assets/echarts-examples-line-category-gallery.png delete mode 100644 docs/website-design-assets/observable-home-hero-collage.png delete mode 100644 docs/website-design-assets/observable-plot-gallery-line-moving-average.png delete mode 100644 docs/website-design-assets/vega-lite-example-gallery-index.png delete mode 100644 docs/website-design-assets/vega-lite-simple-bar-chart-example.png delete mode 100644 docs/website-design-plan.md delete mode 100644 packages/flint-js/src/docs/test_plan.md diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index 2c52ddb6..0aa9c3de 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -68,5 +68,7 @@ Start with the guide that matches the surface you want to extend: ## Test coverage - **Smoke tests:** `packages/flint-js/tests/smoke.test.ts` -- **Visual coverage:** [Gallery](/gallery), driven by `TEST_GENERATORS` in test-data +- **Complete visual coverage:** [Full test cases](/playground/full-test-cases), driven by `TEST_GENERATORS` in `packages/flint-js/src/test-data/` +- **Curated examples:** [Gallery](/gallery) - **Shared fixtures:** `shared/test-data/`, consumed by JS and Python tests +- **Coverage matrices and authoring workflow:** [Chart engine test plan](/documentation/test-plan) diff --git a/docs/adding-a-backend.md b/docs/adding-a-backend.md index 5265869b..c0afb56d 100644 --- a/docs/adding-a-backend.md +++ b/docs/adding-a-backend.md @@ -111,9 +111,13 @@ Register in `templates/index.ts`: import defs, add them to the category map, and # §5 Site and gallery - **Gallery dev server:** `npm run site` from the repo root, then open `/gallery` +- **Full visual matrix:** open `/playground/full-test-cases`; it renders every generator registered in `packages/flint-js/src/test-data/index.ts` - **Supported backends:** update `site/src/shared/supported-backends.ts` if the new backend should appear in the UI - **Renderers:** only add a new React view (`site/src/components/`) when the spec format cannot reuse `VegaLiteView`, `EChartsView`, or `ChartjsView`. `TripleChart` currently covers VL + ECharts + Chart.js. +Use the [Chart engine test plan](/documentation/test-plan) to choose normal, +semantic, density, and edge-case coverage for backend bring-up. + Optional: wire the assembler into `agent-skills/mcp-server/` if MCP clients should be able to call it. --- @@ -134,5 +138,6 @@ A backend is ready when: # §7 Related - [Extending chart templates](/documentation/adding-a-chart-template) — `ChartTemplateDef` authoring +- [Chart engine test plan](/documentation/test-plan) — shared cases and backend bring-up coverage - [Auto Layout Algorithm](/documentation/layout-model) — what `computeLayout()` expects - [API reference](/documentation/api-reference) — `ChartAssemblyInput` and assembler entry points diff --git a/docs/test_plan.md b/docs/test_plan.md index 8c795dc1..122b00b2 100644 --- a/docs/test_plan.md +++ b/docs/test_plan.md @@ -2,12 +2,33 @@ ## Overview -Test data lives in `test-data/` as fixture generators (not executable test suites). -Each file exports generator functions that produce `TestCase[]` arrays. The gallery -UI (`ChartGallery.tsx`) uses `TEST_GENERATORS` and `GALLERY_SECTIONS` from -`test-data/index.ts` to render all tests interactively. - -**20 test-data files**, **~11,100 lines**, **53 named test generators**. +Visual test data lives in `packages/flint-js/src/test-data/` as fixture generators +(not executable test suites). Each case module exports generator functions that +produce `TestCase[]` arrays. The master `TEST_GENERATORS` registry in +`packages/flint-js/src/test-data/index.ts` exposes those cases to the gallery, +editor examples, documentation figures, and the dev playground. + +The current registry contains **31 case modules**, **126 generator groups**, and +**931 generated cases**. Open the site at `/playground/full-test-cases` to inspect +the complete reference set interactively. Each section renders lazily and selects +the first backend that supports its chart type. + +### Backend bring-up workflow + +When adding a backend or porting a chart template: + +1. Add or reuse cases in `packages/flint-js/src/test-data/` that cover the chart's + normal shape, semantic variants, density/cardinality limits, and edge cases. +2. Export the generator and register it in `TEST_GENERATORS` in `test-data/index.ts`. +3. Run `npm run site`, then inspect `/playground/full-test-cases` for broad visual + coverage and `/gallery` for curated product-facing examples. +4. Add focused executable assertions under `packages/flint-js/tests/` for artifact + shape and backend-specific behavior. +5. Run `npm run typecheck` and `npm run test` from the repository root. + +The playground is a visual regression surface, not a replacement for executable +tests. A backend is ready only when both the shared cases render correctly and its +focused test suite passes. ### Test categories diff --git a/docs/website-design-assets/antv-g6-example-editor-three-column.png b/docs/website-design-assets/antv-g6-example-editor-three-column.png deleted file mode 100644 index 5aaad95b7fa4cc727a9cacd9017cf73b10b393c8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 115848 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

vj|2Uq7dQ-SD*PD<3?yTm{gz_T)9ZT5we)#Uj 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

%Sw4{^PxWvjhF72+G|r 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 E4@|VxY5)KL diff --git a/docs/website-design-assets/antv-g6-gallery-grid.png b/docs/website-design-assets/antv-g6-gallery-grid.png deleted file mode 100644 index c94fb71b81e1da9cbf29d78468c9b29394dd3348..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 69282 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

tD{t)M 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 z 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 T)*$N$KklFM0=;3FKW6?1VevqO diff --git a/docs/website-design-assets/echarts-example-editor-option-preview.png b/docs/website-design-assets/echarts-example-editor-option-preview.png deleted file mode 100644 index 2a4d4038daf3213d662446ad43aaee38a2e8644d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 43428 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 diff --git a/docs/website-design-assets/echarts-examples-line-category-gallery.png b/docs/website-design-assets/echarts-examples-line-category-gallery.png deleted file mode 100644 index 1ab005a19c78c24fc0cfa93c7fedf5ce419935cf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 93983 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 diff --git a/docs/website-design-assets/observable-home-hero-collage.png b/docs/website-design-assets/observable-home-hero-collage.png deleted file mode 100644 index 56ff44281f237525e8f0a3dc4e557913ecae7e27..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 112100 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�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

XF>O3O>ggd4JVu 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�@;vft5YnG)^vUr~jeMQ?o8tb%@HgQaXX7-DZ~@ZLijCH= zRoT50Yl|SYJ>BaOngo3no zj|}zNrRRC}hg012{oNv8-cugL5D|(<+B@OTIliH+vDtS7IDmY&ESDYe?7zoRXj*(v z>>2hN2L2&*k1psISwO2zcO;hoR*C=q;4`l9*@5fvZ(dmp*QZkOEK4={$M6ltu%E&ZCbg?dy z$$UuGO=536BcRsuS^^|goQ3#UsBDu}h{k|r9GUd&aO17hk524-ERSofwkDTv;xY4|>3YxK_QvOUxVEGX zoqI`(wl`0c>3-@zcdO0rC9XiHWFMs+oo2v`Jgo427{b07Wl9L>)0!KYyCm(#b~77|ny*W!2hWLM?L8}ww2d$WSzhIVsEo(qQ_^lgz?gdi=P`SwE9t_Q z%`sQxx@(K#q^F^B`O}mKr6SLs@;v`;BN~8AFQK1as4oiOC6p@f#l7uCqNT3vp73g&SkGUeUsKS5*LfyC!z3ZMTkY~27TN3PqqlJ=p!7KT zZD}ZZ;ETECiH+DO|LijWFuBjD9+iJy*5M!7*^(>xDF`KE}YP+`puZvgIui`puhWpNOcB znOc%Gg)G=EME1^PL%Pu2Ig+%Dg{V^6(;~JnZED#tZh(rAg^v2~QUuOi-uTa@OLUyv zyb0}@*iW2YeeCwSf?JzWiSA@?<=2-zurf$YoB~ou>PEKqCnfZ{8ByZ7UtzV`;x`?l z&?AXu&H@zu8uVW^h=ktBuy2;=>0kJzU$`9bkYQMzT<#R$7n99Oom|fZq7ye(-&NE# znMv72>)T>4P+#oO(hYQcaL5#HM)u4j){g!-b!Q31h+9swG>Jjm4?|Z=#LXiXnbR1} z!P0zg@l~Kg!6;#Px{X58(zI$Lu)|?{rf0VC8p>C$*epm!ojn`*izh_O!pdeJLRrSX z!!)1=I|x+||6DA_wCQj3EStL7i%i&PTP-sv0$T-=!?PN5OMVgib${IDaI%NZ)Td|X zm*ZVpb)L&V)cCdaj}exyIpo!~dt`e5rh{~f<5zlhW51tCW?522+j4?)nyJv{-OG$h z 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 R-*m;n@BXJt!_eQj{{tA`lePc= diff --git a/docs/website-design-assets/vega-lite-simple-bar-chart-example.png b/docs/website-design-assets/vega-lite-simple-bar-chart-example.png deleted file mode 100644 index 29d70dd86c9ec3cce05e244805ad1ad9d912639a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 57146 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 diff --git a/docs/website-design-plan.md b/docs/website-design-plan.md deleted file mode 100644 index 937ca786..00000000 --- a/docs/website-design-plan.md +++ /dev/null @@ -1,320 +0,0 @@ -# Flint Chart 官网 / 示例站设计计划(Gallery + Live Editor) - -> **状态:** 设计草案(仅规划,未改代码) -> **日期:** 2026-05-14 -> **范围:** `examples/gallery`、`examples/editor` 的体验与叙事;可选与主文档站或 GitHub Pages 联动 - ---- - -## 1. 产品目标(读者应在短时间内建立何种认知) - -1. **Flint 表达意图更短**:输入为「表格数据 + 字段级 `semantic_types` + 高层 `chart_spec`」,由编译器生成各后端的完整图表配置;用户无需从零编写 Vega-Lite 的 `encoding`、`scale`、`axis`、`legend` 等冗长 JSON。 -2. **默认可读、观感专业**:Gallery 以**大图、清晰栅格、统一留白**为主,避免多枚小图并列削弱第一印象(与 AntV 示例站强调「开箱观感」的方向一致)。 -3. **与「手写 Vega-Lite」对照**:在**同一数据集与相近读图任务**下,用简短文案与(可选)编译产物并排,说明 Flint 如何把版式、格式、色板等决策前移到语义层与推荐逻辑,从而减少 spec 篇幅与决策分支。(Vega-Lite 对部分通道有类型推断与默认样式,但对业务友好的刻度、标签、色盘等,实践中仍常需显式配置;对比时表述应实事求是,见第 9 节。) -4. **Semantic types 有稳定入口**:用侧栏锚点、折叠长文或子路由之一,讲清 T0 / T1 / T2 分层与降级,并链接至 `design-semantics.md` 供深读。 - ---- - -## 2. 参考站点调研 - -下列外链指向各产品官方站点;**截图已放入本仓库** `docs/website-design-assets/`(含 AntV G6、Vega-Lite、Observable、**Apache ECharts**),在 GitHub 或本地 Markdown 预览中应相对于本文路径加载(若 IDE 预览不显示,请确认以仓库根目录打开工作区,并检查 Markdown 预览安全设置)。 - -### 2.0 AntV G6:图表示例栅格与「示例 + 编辑器」三栏工作台 - -[G6 图可视化引擎](https://g6.antv.antgroup.com/) 的「图表示例」与单示例页,在信息架构上同时承担 **检索示例** 与 **就地改代码**。 - -**图 1 — 图表示例首页:左侧分类 + 主区卡片栅格(缩略图 + 标题)** - -![AntV G6 图表示例:左侧分类导航与主区示例卡片栅格](./website-design-assets/antv-g6-gallery-grid.png) - -| 可借鉴点 | 对应本文章节 | -|----------|----------------| -| 侧栏多级分类(特性、场景案例、布局、交互等) | 第 5 节 Gallery、第 7 节导航 | -| 缩略图与短标题、主区分组标题 | 第 5.3 节(主区内视觉层级) | -| 顶栏:文档 / API / 示例 / 社区、搜索 | 第 7 节入口 | - -**图 2 — 单示例页:左导航、中大预览、右侧代码(JavaScript / Data 分 Tab)** - -![AntV G6 示例页:左导航、中画布、右侧代码与 Data Tab](./website-design-assets/antv-g6-example-editor-three-column.png) - -| 可借鉴点 | 对应本文章节 | -|----------|----------------| -| 预览占据视觉中心 | 第 4 节 Editor、第 5.3 节 | -| 代码与 **数据** 分 Tab | `data` 与 `semantic_types` 分区展示的参考 | -| 复制、运行、展开等工具条 | 第 8 节 P1 之后可增强 | - -**相关官方链接** - -- [G6 API · 数据](https://g6.antv.antgroup.com/api/data)(数据模型与 API 目录,可作「数据 / 语义」文档信息架构的参考) -- [G6 官网](https://g6.antv.antgroup.com/) -- [G2 图表示例(英文)](https://g2.antv.antgroup.com/en/examples)(通用统计图示例矩阵) - -> 上列截图为 AntV 官方界面,仅作设计与动线参考。 - -### 2.2 Vega-Lite:Example Gallery 与单示例页(含官方截图) - -[Vega-Lite](https://vega.github.io/vega-lite/) 的文档与 [Example Gallery](https://vega.github.io/vega-lite/examples/) 是 Flint 用户最熟悉、也最适合作为 **「手写 spec」对照物** 的参照;[Vega Editor](https://vega.github.io/editor/) 提供在线编辑与分享。 - -**图 3 — Example Gallery 索引区:以分级列表为主的目录页** - -![Vega-Lite Example Gallery:顶栏导航与按类目展开的示例链接列表](./website-design-assets/vega-lite-example-gallery-index.png) - -| 观察 | 对 Flint 的启示 | -|------|------------------| -| 类目完整、便于检索(Single-View、Layered、Facet、Interactive 等) | 保留「按场景 / generator 分组」的清晰度 | -| 首屏以文字目录为主,缩略图密度低于 AntV G6 图表示例首页 | Flint Gallery 若以 **缩略图 + 短标题** 为主,更易形成「一眼看到图」的差异 | -| 顶栏含 Documentation、Examples、Try Online 等 | 全局导航与「一键试玩」入口值得对齐 | - -**图 4 — 单示例页(Simple Bar Chart):图 + 说明 + 在线编辑器链接 + JSON 规格** - -![Vega-Lite 官方示例「Simple Bar Chart」:渲染结果与 Vega-Lite JSON Specification 片段](./website-design-assets/vega-lite-simple-bar-chart-example.png) - -| 观察 | 对 Flint 的启示 | -|------|------------------| -| 「View this example in the online editor」类链接 | 对应 Flint 第 5.1 / 5.4 节与第 7 节「Gallery → Editor」深链 | -| 即使简单柱状图,spec 仍包含 `$schema`、`data`、`mark`、`encoding` 等完整结构 | 对应第 4 节:在 Editor 中并排展示 **Flint 输入** 与 **编译得到的 Vega-Lite**,用篇幅对比体现「少写」 | -| 默认视觉偏文档演示风格 | Flint 若以默认观感与版式为卖点,需在 Gallery 用真实数据与统一主题证明 | - -**相关官方链接** - -- [Vega-Lite Example Gallery](https://vega.github.io/vega-lite/examples/) -- [Vega Editor](https://vega.github.io/editor/) -- [Vega-Lite 文档首页](https://vega.github.io/vega-lite/docs/) - -**设计取舍(文字摘要)** - -- Gallery 可沿用「按图表类型与复合视图能力分块」的思路,但首屏需有一句 **Flint 定位**,避免被误读为普通测试列表。 -- Editor 侧可学习 Vega Editor:**错误信息固定区域**、**示例切换不改变整体框架**。 -- 后续可在 `website-design-assets/` 增补「同一用例:Flint 输入 vs 生成 VL」的自制对比图,与上列官方截图并列说明。 - -### 2.3 Observable:首页叙事与 Observable Plot 示例(含官方截图) - -[Observable](https://observablehq.com/) 以响应式 Notebook 为核心;[Observable Plot](https://observablehq.com/plot/) 提供声明式 `Plot.plot({...})` API;示例合集以 Notebook 形式发布,例如官方 [Plot Gallery](https://observablehq.com/@observablehq/plot-gallery),将「结果图 + 简短代码」紧挨展示,与 Flint 希望的「先看到图、再看到多短能写出来」一致。Flint 仍以 **JSON 装配输入 + 多后端编译** 为主,不复制 Notebook 运行时;以下仅借鉴 **版式、首屏叙事与示例节奏**。 - -**图 5 — Observable Plot Gallery 单例:「Line with moving average」(图在上、代码在下)** - -![Observable Plot Gallery:Line with moving average 示例页,含散点、滑动平均线与 Plot.plot 代码块](./website-design-assets/observable-plot-gallery-line-moving-average.png) - -| 观察 | 对 Flint 的启示 | -|------|------------------| -| 面包屑 `OBSERVABLE PLOT > GALLERY`、标题与一段数据来源说明 | Gallery 用例页:**类目路径 + 一句话场景/数据来源**,再进入图与配置 | -| 图下方紧跟短代码(`marks`、`Plot.windowY` 等),左侧有运行/展开类控件 | Editor:**预览与输入同屏**;Flint 若用 JSON,仍可学习「图与配置垂直相邻、减少视线跳跃」 | -| 声明式 API、色板与参考线在少量行内表达 | 与第 1、4 节一致:强调 **意图层压缩**;Flint 用 `semantic_types` + `chart_spec` 而非手写 Plot 或 VL 细节 | - -**图 6 — Observable 官网首屏:主文案 + CTA + 图与代码拼贴背景** - -![Observable 官网首屏:深色网格背景、主标题与 Try it for free / Explore the docs 按钮及多图拼贴](./website-design-assets/observable-home-hero-collage.png) - -| 观察 | 对 Flint 的启示 | -|------|------------------| -| 强主标题与副文案,双按钮(试用 / 读文档) | GitHub Pages 或落地页:**一句定位 + Gallery / Editor / 文档** 主次按钮 | -| 背景拼贴多枚高质量缩略图 | Gallery 或首页:**多图氛围**与单卡大图主预览可结合(注意性能与首屏加载) | -| 中央卡片内「图 + 代码」一体化 | 与图 5 同理:强化「Flint 输入很短、图很清晰」的一体展示(不必采用 Observable 的深色品牌) | - -**相关官方链接** - -- [Observable 平台](https://observablehq.com/) -- [Observable Plot](https://observablehq.com/plot/) -- [Plot Gallery(Observable Notebook)](https://observablehq.com/@observablehq/plot-gallery) - -**设计取舍(文字摘要)** - -- **可学**:渐进式教程动线、首屏 CTA、示例页「先图后码」的信息顺序。 -- **不必照搬**:完整 Notebook 工作区、Fork/星标社区流、运行时单元格依赖;Flint MVP 保持 **静态示例站 + 本地/单页 Editor** 即可。 - -### 2.4 Apache ECharts:Examples 画廊与在线编辑器(含官方截图) - -[Apache ECharts](https://echarts.apache.org/en/index.html) 的 [Examples(英文索引)](https://echarts.apache.org/examples/en/index.html) 与单例在线编辑器,是 Flint **ECharts 后端**用户已熟悉的「配置项 `option` + 即时预览」范式;与 Vega-Lite 的声明式 spec 不同,ECharts 更偏 **命令式配置对象**,同样适合在 Flint Editor 中作为 **「编译输出对照」** 或「多引擎 Tab」心智参考(本仓库 `assembleECharts` 已存在)。 - -**图 7 — Examples:侧栏图表族(含图标)+ 主区缩略图栅格(以 Line 类目为例)** - -![Apache ECharts Examples:左侧 Line 等类目与图标,主区多列缩略图与标题](./website-design-assets/echarts-examples-line-category-gallery.png) - -| 观察 | 对 Flint 的启示 | -|------|------------------| -| 侧栏按图表类型分组并配小图标,当前类目高亮 | Gallery:**一眼可扫的分类 + 当前位置**;可与 generator / 场景树结合 | -| 主区多列缩略图 + 短标题,同类目下变体丰富 | 第 5.3 节:主区内 **图优先** 的卡片预览;侧栏「按类型」见第 7 节 | -| 主内容区提供 **Dark mode** 等主题切换 | Flint Gallery / Editor 可提供浅色 / 深色预览,验证默认可读性与导出一致性 | - -**图 8 — 单示例在线编辑器:左侧 `option` 代码、右侧实时渲染** - -![Apache ECharts 在线编辑器:左侧 option 与 Run,右侧折线图预览及工具条](./website-design-assets/echarts-example-editor-option-preview.png) - -| 观察 | 对 Flint 的启示 | -|------|------------------| -| 经典 **左码右图** 分栏,与 Gallery 点进示例后的工作台一致 | 与第 4、5.4、7 节「Gallery → Editor」及并排对照布局一致,降低学习成本 | -| Tab:Edit Code / Full Code / Option Preview;语言 JS / TS;**Run** 显式触发刷新 | 可学习:**错误与运行反馈**、全量配置与「最小片段」切换;Flint 若以自动编译为主,仍可保留「手动刷新 / 防抖」选项 | -| 基础折线亦需配置 `xAxis`、`yAxis`、`series` 等 | Editor 中展示 **Flint 输入 vs 生成的 ECharts option** 时,可并列说明「意图层」如何展开为轴与系列 | -| 预览区附带下载、截图、分享、渲染耗时等 | 第 8 节 P1 之后可选增强,利于演示与 issue 复现 | - -**相关官方链接** - -- [Apache ECharts · Examples(英文索引)](https://echarts.apache.org/examples/en/index.html) -- [Apache ECharts 官网](https://echarts.apache.org/en/index.html) -- [Handbook(入门与概念)](https://echarts.apache.org/handbook/en) -- [Option Manual(配置项手册)](https://echarts.apache.org/en/option.html) - -**设计取舍(文字摘要)** - -- **可学**:图类型侧栏 + 缩略图矩阵、在线编辑器分栏、主题切换与导出类工具。 -- **边界**:ECharts 的 `option` 体量与 VL 不同维度的「冗长」;Flint 叙事应对 **各后端编译产物** 分别诚实展示,避免只对比 VL 而忽略 ECharts 用户的体感。 - -### 2.5 行业常见模式(摘录) - -- 示例页提供「在 Playground / Editor 中打开」。 -- 深链或查询参数传递示例标识,便于分享与从 Gallery 跳入(注意 URL 长度,见第 9 节)。 -- **响应式**:Gallery 可单列堆叠即可;Editor 以桌面宽屏为主,MVP 可明确写清设备优先级。 - ---- - -## 3. 与当前仓库实现的关系 - -- **Gallery**:已有左侧 `TEST_GENERATORS` 键列表、`TripleChart` 三后端并排、`tests.slice(0, 6)` 限制展示条数;源码注释标明仍为 scaffold。**规划变更(见第 7 节)**:左侧改为 **按图表类型** 的侧栏(类 ECharts),主区 **仍保留** 当前「用例标题 + 描述 + `TripleChart`」卡片形态;需在数据层维护 **图表类型 ↔ generator / 用例** 的映射(可从 `chart_spec.chartType`、generator 名规则或单独配置表推导,实现时选定唯一来源)。 -- **Editor**:左侧为 `ChartAssemblyInput` 的 JSON(CodeMirror),右侧为后端 Tab(Vega-Lite / ECharts / Chart.js)及可选编译 spec;内置示例见 `examples/editor/src/examples.ts`。 -- **语义设计文档**:`docs/design-semantics.md` 描述 T0 / T1 / T2 与降级策略,适合作为站外或 `/docs` 的权威说明;示例站内页以摘要与链接为主,避免重复维护长文。 - ---- - -## 4. Live Editor:并排展示「Flint 输入」与「生成的 Vega-Lite」 - -### 4.1 布局建议(控制复杂度) - -在现有「左编辑 / 右预览」基础上,将 **左侧** 拆为 **上下两块**(较左右分栏更不易挤压行宽): - -| 区域 | 内容 | -|------|------| -| **上:Flint 输入** | 可编辑 JSON:`data`、`semantic_types`、`chart_spec`(与当前行为一致) | -| **下:对照** | **只读**、格式化的 **由当前输入编译得到的 Vega-Lite spec**;标题建议写作「Vega-Lite 输出(由 Flint 生成)」,避免被误解为用户手写的最简 spec | - -用户心智:**只维护上一份意图描述**;下方为同一意图在当前编译器下的 VL 展开结果,用于感受篇幅与结构复杂度。 - -**第二阶段可选** - -- 低调展示行数或嵌套层级对比。 -- 「复制 Vega-Lite」按钮,便于在 Vega Editor 中交叉验证。 - -### 4.2 错误与空状态 - -- JSON 语法错误:继续在输入区附近展示解析错误(与现状一致)。 -- 编译失败:保留 Flint 输入可编辑;对照区展示失败原因,避免整页空白。 - ---- - -## 5. Gallery - -### 5.1 布局约束 - -- **全局**:全站 **顶部导航栏**(类 Vega-Lite:Gallery、Tutorials、Documentation、Usage / Getting started、Ecosystem、GitHub、Try / Editor 等,实现时可按 MVP 裁剪条目)。 -- **Gallery 页内**:**左侧** 为 **按图表类型** 分类的侧栏(类 ECharts:类目 + 可选小图标、当前选中高亮);**右侧主区** **保持与当前 `examples/gallery` 相同** 的用例展示——即每个用例仍以 **标题、描述、`TripleChart`(或等价多后端预览)** 为主的纵向卡片流,**不**把主区改成 ECharts 官网那种纯缩略图矩阵(避免与「和现在 gallery 一样」冲突)。 -- **逐例进入 Editor**:在每个用例 **标题同一行末尾** 或 **标题下第一行**,放置与 Vega-Lite 单示例页同款的文案链:**「View this example in the online editor」**(中文站可并列或单独提供「在在线编辑器中打开」)。点击后 **进入当前 Editor 应用界面**(同仓库 `examples/editor`:开发时为另一 Vite 端口或子路径 `/editor`,上线时为统一域名下的 Editor 路由),并载入该用例对应的 `ChartAssemblyInput`(载荷方式见 §5.4)。 - -### 5.2 视觉层级(主区内) - -- **主预览**:默认突出单一后端(例如 Vega-Lite)或 Flint 首推导出效果;ECharts、Chart.js 作为 Tab 或次级折叠,避免三列长期同权导致「测试页」观感。 -- **多引擎**:以「另存为其他引擎」或「多引擎」Tab 呈现,强调能力边界而非首屏噪音。 - ---- - -## 6. Semantic types:放置位置与内容边界 - -### 6.1 放置方案比较 - -| 方案 | 优点 | 缺点 | -|------|------|------| -| **A.** Gallery 页侧栏 **图表类型区下方** 或主区顶部固定「Semantic types」入口 + 页内长折叠 | 曝光高;与第 7 节侧栏并存时需控制高度(可折叠「高级 / 语义」区) | 侧栏信息密度上升 | -| **B.** 独立路由(如 `/semantic-types`) | 内容易扩展 | 多一页导航与构建配置;需放入顶栏 **Documentation** 子链或主导航 | -| **C.** 仅链至 `docs/design-semantics.md` | 维护单点 | 离开示例站语境 | - -**MVP 建议:** **A**(与图表类型侧栏分区排版);内容膨胀后再引入 **B**,并在顶栏 **Documentation** 中链入。 - -### 6.2 示例站页内大纲(精简) - -1. 一句话:字段语义类型如何影响格式、聚合、基线、色板类别等默认决策。 -2. T0 / T1 / T2:由粗到细,**缺失细粒度类型时降级而非失败**。 -3. 一至两个静态对照(如仅标为数值与标注为 `Amount` / `Proportion` 时的轴与标签差异)。 -4. 指向 `design-semantics.md` 全文。 - ---- - -## 7. 信息架构:顶栏(类 Vega-Lite)+ Gallery(侧栏类 ECharts、主区保持现状) - -### 7.1 全站顶部导航栏 - -对齐 [Vega-Lite 文档站](https://vega.github.io/vega-lite/) 顶栏信息结构(不必逐项同名,但类目宜接近用户心智): - -| 导航项 | 用途(建议) | -|--------|----------------| -| **Gallery**(或 **Examples**) | 进入示例画廊(本页为站内核心流量入口之一)。 | -| **Tutorials** | 分步入门、常见数据集走通;MVP 可单页或外链。 | -| **Documentation** | API / 装配输入 schema / 链至仓库 `docs/` 与 `design-semantics.md`。 | -| **Usage**(或 **Getting started**) | 安装、一行代码渲染、与框架集成要点。 | -| **Ecosystem** | 相关工具、后端矩阵、Roadmap;可精简为单页。 | -| **GitHub** | 源码仓库。 | -| **Try online** / **Editor** | **直达** 在线 **Editor** 根路由(空白或默认模板);与 Gallery 内 **「View this example in the online editor」**(带用例上下文)区分。 | - -可选:**搜索**、**中 / 英文**(非 MVP 可后做)。 - -### 7.2 Gallery 页:侧栏 + 主区 - -``` -Gallery 路由(/ 或 /gallery) -├── 顶栏(见 §7.1,全站一致) -├── 左侧边栏 -│ ├── 按「图表类型」分组(参考 ECharts Examples:Line、Bar、Scatter…;类目映射见第 3 节) -│ └── [可选] Semantic types 入口(见第 6 节) -└── 主内容区(与当前实现一致) - └── 当前选中类型下的用例列表:标题 + 描述 + TripleChart(或等价) - └── 每例标题旁:「View this example in the online editor」→ Editor + 该例载荷 -``` - -- **侧栏**:负责 **「按图表类型找例」**;可保留图标、选中态、与 ECharts 类似的纵向类目列表。 -- **主区**:**不改为**缩略图-only 的矩阵;延续 **大卡片 + 多后端预览**,以满足「图表展示还是和现在 gallery 一样」的产品要求。 -- **逐例链接**:文案 **「View this example in the online editor」** 与 Vega-Lite 单例页一致,便于用户迁移习惯;点击跳转 **现有 Editor 界面**(见 §5.4)。 - -### 7.3 Editor 路由(建议) - -``` -/editor(或独立子应用 + 统一域名) -├── Flint 输入(JSON) -├── 生成的 Vega-Lite(只读对照,见第 4 节) -└── 预览(主后端 + 多引擎 Tab) -``` - -- 从 **顶栏 Try / Editor** 进入:无 `generator` / `case` 参数时加载默认空模板或内置第一个示例。 -- 从 **Gallery 逐例链接** 进入:携带 §5.4 约定参数,自动填充该用例 `ChartAssemblyInput`。 - -### 7.4 落地页与仓库入口 - -- 对外首页或 GitHub Pages:**一句定位 + 顶栏或首屏 CTA** 指向 **Gallery** 与 **Editor**;与 Observable 式双按钮可并存,但 **全局顶栏** 为信息架构主轴。 - ---- - -## 8. 分阶段交付 - -| 阶段 | 交付内容 | 验收要点 | -|------|----------|----------| -| **P0** | Editor 左栏:Flint 输入 + 只读生成 VL;Gallery 顶部对比折叠带 | 新用户能在约半分钟内理解「维护单份输入」 | -| **P1** | 全站 **顶栏**(Gallery / Tutorials / Documentation / …);Gallery **按图表类型侧栏**;主区保持现有卡片 + `TripleChart`;每例标题处 **「View this example in the online editor」** 深链至 Editor 且可复现该例 | 任意展示用例一键进入 Editor 且状态一致 | -| **P2** | Gallery 主图 + 多引擎 Tab;精选用例的补充说明 | 整体更接近产品站而非内部测试列表 | -| **P3** | Semantic types 独立短页或小册式滚动区 + 图示 | 清楚区分「统计类型 Q/N/O/T」与 Flint「业务语义类型」的角色 | - ---- - -## 9. 风险与表述原则 - -- **载荷与隐私**:大 JSON 避免直接塞进 URL;可用 `sessionStorage` 或短键服务端交换(若未来有后端)。 -- **对比诚实性**:对照区展示 **当前版本编译器真实输出**;不虚构「手写 VL 最少行数」。Vega-Lite 具备强表达力与部分默认行为,叙事重点放在 **意图层压缩与默认决策外置**,而非贬低 VL。 -- **维护成本**:卡片文案以模板与数据驱动为主,少量手写「标杆用例」即可。 - ---- - -## 10. 实现阶段可执行项(备忘) - -1. `examples/editor`:基于已有 `assembleVegaLite` 结果序列化至只读面板;解析 URL 查询参数或 `sessionStorage` token,**预填**来自 Gallery 的 `ChartAssemblyInput`。 -2. `examples/gallery`:增加 **全站顶栏** 组件;左侧 **图表类型** 侧栏(数据源:类型 ↔ generator/用例映射);主区沿用现有卡片结构;每例标题行渲染 **「View this example in the online editor」** 并指向 Editor(开发环境需处理跨端口 URL 或反向代理为同域)。 -3. `examples/gallery` 与 `examples/editor`:抽取共享的注册表与「打开 Editor」载荷编码(可考虑 `examples/shared` 等小模块)。 -4. 样式:提取有限 CSS 变量(背景、边框、主色),与 `docs/landing.html` 等品牌触点可选对齐。 - ---- - -**文档性质:** 设计与调研归档;实施顺序以第 8 节为准,**本文不替代** `design-semantics.md` 中的语义类型技术定义。 diff --git a/packages/flint-js/src/docs/test_plan.md b/packages/flint-js/src/docs/test_plan.md deleted file mode 100644 index 6777d8df..00000000 --- a/packages/flint-js/src/docs/test_plan.md +++ /dev/null @@ -1,369 +0,0 @@ -# Chart Engine Test Plan - -## Overview - -Test data lives in `test-data/` as fixture generators (not executable test suites). -Each file exports generator functions that produce `TestCase[]` arrays. The gallery -UI (`ChartGallery.tsx`) uses `TEST_GENERATORS` and `GALLERY_SECTIONS` from -`test-data/index.ts` to render all tests interactively. - -**20 test-data files**, **~11,100 lines**, **53 named test generators**. - -### Test categories - -| Category | Files | Description | -|----------|-------|-------------| -| **VL chart matrices** | scatter-tests, line-tests, bar-tests, area-tests | Matrix-driven tests for core VL chart types | -| **Distribution charts** | distribution-tests | Histogram, Boxplot, Density, Strip Plot | -| **Specialized charts** | specialized-tests | Pie, Heatmap, Lollipop, Candlestick, Waterfall, Ranged Dot, Bump, Radar, Pyramid, Rose, Custom | -| **Semantic context** | semantic-tests | 39 tests validating semantic type → ChannelSemantics resolution | -| **ECharts backend** | echarts-tests | All ECharts chart types (reuses VL inputs + ECharts-only types) | -| **Chart.js backend** | chartjs-tests | Chart.js chart types | -| **Facets** | facet-tests | Column, row, col+row, wrap, clip, overflow faceting | -| **Stress/sizing** | stress-tests, gas-pressure-tests, line-area-stretch-tests, discrete-axis-tests | Overflow, elasticity, pressure model, discrete axis sizing | -| **Temporal** | date-tests | Year, Month, YearMonth, Decade, DateTime, Hours parsing/formatting | -| **Line/area variants** | line-area-tests | Bump Chart | - ---- - -## Scatter Plot - -A scatter plot places marks (points/bubbles) in a 2D space. The core axes are continuous (quantitative), but one or both axes can be discrete (nominal), which changes how the engine computes layout, step sizing, and overflow. - -Temporal axes are omitted from scatter tests because T behaves identically to Q in scatter layout — no special handling. Temporal is still tested as a color channel (`color: 'T'`). - -Default test canvas: 300 × 300 px. - -### Matrix-driven approach - -Tests are generated from a **declarative matrix** (`SCATTER_MATRIX` in `scatter-tests.ts`). Each row describes one test via its axis types, optional third channels, cardinality, and special flags. A generator function converts each matrix entry into a full `TestCase`. - -### Matrix dimensions - -| Dimension | Values | Notes | -|-----------|--------|-------| -| **x axis type** | Q, N | Quantitative, Nominal | -| **y axis type** | Q, N | Same | -| **color channel** | —, Q, T, N | Optional 3rd encoding | -| **size channel** | —, Q, N | Optional 4th encoding | -| **n (density)** | 10–500 | Or 0 for N×N grid mode | -| **cardinality** | xCard, yCard, colorCard, sizeCard | Cardinality of nominal dims | -| **flags** | hugeRange | Special data distributions | - -### Full test matrix (25 tests) - -#### Q × Q — 15 tests - -| # | color | size | n | flags | what it tests | -|---|-------|------|---|-------|---------------| -| 1 | — | — | 20 | | Baseline scatter | -| 2 | N(3) | — | 20 | | Nominal color groups | -| 3 | Q | — | 20 | | Continuous color gradient | -| 4 | T | — | 30 | | Temporal color gradient | -| 5 | — | Q | 50 | | Bubble chart | -| 6 | — | N(4) | 20 | | Ordinal size — 4 ranked levels | -| 7 | N(3) | Q | 15 | | Gapminder-style | -| 8 | Q | Q | 30 | | Dual continuous (4D) | -| 9 | N(20) | Q | 20 | hugeRange | Size 1K–1B, sqrt scale | -| 10 | — | — | 100 | | Moderate density | -| 11 | — | — | 500 | | High density | -| 12 | N(20) | — | 200 | | Dense, many groups | -| 13 | N(50) | — | 100 | | Legend overflow | -| 14 | — | Q | 10 | | Sparse bubbles | -| 15 | — | Q | 200 | | Dense bubbles | - -#### N × Q — 4 tests - -| # | xCard | color | size | n | what it tests | -|---|-------|-------|------|---|---------------| -| 1 | 5 | Q | — | 25 | Strip + continuous color | -| 2 | 5 | — | Q | 25 | Bubble strip | -| 3 | 2 | — | — | 30 | Binary category strip (edge) | -| 4 | 60 | — | — | 60 | 60 cats — overflow | - -#### Q × N — 3 tests (mirrors N×Q with flipped orientation) - -| # | yCard | color | size | n | what it tests | -|---|-------|-------|------|---|---------------| -| 1 | 5 | Q | — | 25 | Horizontal strip + continuous color | -| 2 | 5 | — | Q | 25 | Horizontal bubble strip | -| 3 | 60 | — | — | 60 | Horizontal 60-cat overflow | - -#### N × N — 3 tests - -| # | xCard | yCard | color | size | what it tests | -|---|-------|-------|-------|------|---------------| -| 1 | 5 | 6 | — | Q | Bubble grid | -| 2 | 5 | 4 | Q | — | Heatmap-like grid | -| 3 | 15 | 12 | — | Q | Large grid — overflow | - -### Coverage summary - -Axis combos: Q×Q, N×Q, Q×N, N×N. Third-channel variants (color and size, typed Q/T/N) crossed with Q×Q. Density from 10 to 500. Edge cases: binary categories, legend overflow, huge value ranges. - -### How to add a test - -Add one row to `SCATTER_MATRIX` in `scatter-tests.ts`: - -```typescript -{ x: 'N', y: 'Q', n: 40, xCard: 8, color: 'Q', desc: 'Strip + continuous color, moderate density' }, -``` - -The generator handles field naming, data synthesis, metadata, tags, and title automatically. - ---- - -## Line Chart - -A line chart connects data points with lines. Lines imply sequential progression, so axes use T (temporal), O (ordinal), or Q (quantitative) — never purely nominal. N (nominal) is used only for color groups. - -Channels: `x, y, color, opacity, column, row` - -### Matrix-driven approach - -Tests are generated from `LINE_MATRIX` in `line-tests.ts`. Each row specifies axis types, optional color channel, point count, and flags like `sparse` (20% dropout). - -### Full test matrix (16 tests) - -#### T × Q — 6 tests (core time series) - -| # | color | n | flags | what it tests | -|---|-------|---|-------|---------------| -| 1 | — | 30 | | Simple time series | -| 2 | N(4) | 200 | | 4 series × 50 dates | -| 3 | N(8) | 800 | | 8 series crowded | -| 4 | N(20) | 4000 | stress | 20 series spaghetti | -| 5 | N(3) | 180 | sparse | 3 series, ~20% missing | -| 6 | Q | 30 | | Continuous color gradient | - -#### O × Q — 4 tests (ordinal x) - -| # | xCard | color | n | what it tests | -|---|-------|-------|---|---------------| -| 7 | 5 | — | 5 | Ordinal line | -| 8 | 12 | N(4) | 48 | 12 ordinal × 4 series | -| 9 | 30 | — | 30 | Label overflow | -| 10 | 5 | Q | 5 | Ordinal + gradient | - -#### Q × Q — 3 tests - -| # | color | n | what it tests | -|---|-------|---|---------------| -| 11 | — | 30 | Quantitative x line | -| 12 | N(3) | 150 | 3 parametric curves | -| 13 | — | 200 | Dense single curve | - -#### Q × O — 3 tests (mirror) - -| # | yCard | color | n | what it tests | -|---|-------|-------|---|---------------| -| 14 | 5 | — | 5 | Horizontal ordinal | -| 15 | 12 | N(4) | 48 | Horizontal 12 ordinal × 4 | -| 16 | 30 | — | 30 | Horizontal 30 ordinal overflow | - -#### Excluded combos - -- **T×T, Q×T** — date-pair data (start vs end date) doesn't suit line charts. Each row is an independent event, not a sequential series; lines connect points in data order producing random zig-zags. Better served by scatter or dumbbell charts. -- **O×O** — ordinal×ordinal lines are degenerate. -- **N×N, T×N, N×T** — purely nominal axes don't suit line charts. Lines imply sequence/progression; connecting unordered categories is misleading. - -### Coverage summary - -Axis combos: T×Q, O×Q, Q×Q, Q×O. Color variants (N, Q) crossed with primary combos. Density from 5 to 4000 (stress). Sparse dropout tests irregular gaps. Total **16 tests**. - ---- - -## Bar Chart / Stacked Bar Chart / Grouped Bar Chart - -Bar charts encode values as rectangular bars. Three variants share a common matrix format in `bar-tests.ts`: -- **Bar Chart**: `x, y, color, opacity` — basic bars with optional color -- **Stacked Bar Chart**: `x, y, color` — bars stacked by color dimension -- **Grouped Bar Chart**: `x, y, group` — bars side-by-side by group dimension - -### Matrix-driven approach - -Three matrices (`BAR_MATRIX`, `STACKED_BAR_MATRIX`, `GROUPED_BAR_MATRIX`) share one generator function `barMatrixToTestCase`. The third channel key is `'color'` for bar/stacked and `'group'` for grouped. - -### Bar Chart matrix (19 tests) - -#### N × Q — 6 tests (classic vertical) - -| # | xCard | color | n | what it tests | -|---|-------|-------|---|---------------| -| 1 | 5 | — | 5 | Basic 5 bars | -| 2 | 20 | — | 20 | Label rotation | -| 3 | 30 | — | 30 | Thin bar handling | -| 4 | 100 | — | 100 | Discrete cutoff | -| 5 | 5 | N(3) | 15 | 5 cats × 3 colors | -| 6 | 5 | N(20) | 100 | Color saturation | - -#### Q × N — 3 tests (horizontal) - -| # | yCard | color | n | what it tests | -|---|-------|-------|---|---------------| -| 7 | 10 | — | 10 | Horizontal 10 bars | -| 8 | 100 | — | 100 | Horizontal cutoff | -| 9 | 10 | N(3) | 30 | Horizontal + 3 colors | - -#### T × Q — 3 tests (temporal) - -| # | color | n | what it tests | -|---|-------|---|---------------| -| 10 | — | 24 | Temporal bars | -| 11 | — | 100 | 100 dates — dynamic sizing | -| 12 | N(3) | 72 | Temporal + 3 colors | - -#### Q × T — 2 tests (horizontal temporal) - -| # | color | n | what it tests | -|---|-------|---|---------------| -| 13 | — | 18 | Horizontal temporal | -| 14 | N(3) | 54 | Horizontal temporal + color | - -#### Q × Q — 2 tests (continuous banded) - -| # | n | what it tests | -|---|---|---------------| -| 15 | 20 | Both quant — dynamic resizing | -| 16 | 30 | Equally spaced 1..30 | - -#### Edge combos — 3 tests - -| # | x | y | n | what it tests | -|---|---|---|---|---------------| -| 17 | N | N | grid | Cat × cat (degenerate) | -| 18 | T | T | 20 | Date × date (degenerate) | -| 19 | T | N | 25 | Temporal × categorical | - -### Stacked Bar Chart matrix (12 tests) - -| # | x | y | color | n | what it tests | -|---|---|---|-------|---|---------------| -| 1 | N | Q | N(3) | 12 | Basic stack 4×3 | -| 2 | N | Q | N(5) | 75 | Large 15×5 | -| 3 | N | Q | N(3) | 240 | Very large 80×3 (cutoff) | -| 4 | N | Q | Q(4) | 24 | Numeric color (1–4) | -| 5 | N | Q | Q(30) | 150 | Numeric color (1–30) | -| 6 | T | Q | N(3) | 30 | Temporal stack | -| 7 | T | Q | N(4) | 80 | 20 dates × 4 | -| 8 | Q | Q | N(3) | 30 | Both quant stacked | -| 9 | Q | N | N(3) | 24 | Horizontal stack | -| 10 | Q | T | N(3) | 45 | Horizontal temporal stack | -| 11 | N | N | N(3) | grid | Cat×cat stacked (edge) | -| 12 | T | T | N(3) | 30 | Date×date stacked (edge) | - -### Grouped Bar Chart matrix (12 tests) - -| # | x | y | group | n | what it tests | -|---|---|---|-------|---|---------------| -| 1 | N | Q | N(3) | 12 | Basic grouped 4×3 | -| 2 | N | Q | — | 8 | No group — fallback | -| 3 | N | Q | N(3) | 270 | Very large 90×3 (cutoff) | -| 4 | N | Q | Q(5) | 30 | Numeric group (1–5) | -| 5 | T | Q | N(3) | 36 | Temporal grouped | -| 6 | Q | Q | N(4) | 20 | Both quant + group | -| 7 | Q | N | N(4) | 24 | Horizontal grouped | -| 8 | Q | T | N(3) | 30 | Horizontal temporal grouped | -| 9 | N | Q | Q(50) | 400 | Numeric group (1–50) | -| 10 | N | Q | Q | 50 | Continuous float on group | -| 11 | N | N | N(3) | grid | Cat×cat grouped (edge) | -| 12 | T | T | N(3) | 30 | Date×date grouped (edge) | - -### Coverage summary - -All three bar variants cover common xy-type combinations. Bar Chart has 19 tests, Stacked Bar has 12, Grouped Bar has 12 — total **43 bar tests**. Covers horizontal/vertical orientation, discrete cutoff, numeric/continuous color, edge combos. - -## Area Chart & Streamgraph - -**File:** `area-tests.ts` -**Approach:** Matrix-driven — `AREA_MATRIX` (17 entries) + `STREAMGRAPH_MATRIX` (6 entries). -**Shared generator:** `areaMatrixToTestCase(entry, chartType, rand)` — same infrastructure for both chart types. -**Data characteristic:** Uses `genAreaTrend()` with upward drift (natural for cumulative / stacked-area metrics). - -Area charts use O (ordinal) for categorical axes (like line charts) — area fills imply continuity. N (nominal) is used only for color groups. Purely nominal axis combos are excluded. - -### Area Chart matrix (17 tests) - -#### T × Q — 7 tests (core stacked / layered area) - -| # | color | n | flags | what it tests | -|---|-------|---|-------|---------------| -| 1 | — | 30 | | Simple time-series area | -| 2 | N(4) | 96 | | 4 stacked series | -| 3 | N(8) | 480 | | 8 series large stacked | -| 4 | N(15) | 1800 | stress | 15 series stress | -| 5 | N(3) | 120 | | 3 layered/overlapping | -| 6 | N(3) | 180 | sparse | 3 series, ~20% missing | -| 7 | Q | 30 | | Continuous color gradient | - -#### O × Q — 4 tests (ordinal x) - -| # | xCard | color | n | what it tests | -|---|-------|-------|---|---------------| -| 8 | 5 | — | 5 | Ordinal area 5 cats | -| 9 | 12 | N(4) | 48 | 12 ordinal × 4 stacked | -| 10 | 30 | — | 30 | 30 ordinal overflow | -| 11 | 5 | Q | 5 | Ordinal + continuous color | - -#### Q × O — 3 tests (mirror) - -| # | yCard | color | n | what it tests | -|---|-------|-------|---|---------------| -| 12 | 5 | — | 5 | Horizontal ordinal 5 cats | -| 13 | 12 | N(4) | 48 | Horizontal 12 ordinal × 4 | -| 14 | 30 | — | 30 | Horizontal 30 ordinal overflow | - -#### Q × Q — 3 tests - -| # | color | n | what it tests | -|---|-------|---|---------------| -| 15 | — | 30 | Quantitative x area | -| 16 | N(3) | 150 | 3 stacked curves | -| 17 | — | 200 | Dense single-series | - -#### Excluded combos - -- **T×T, Q×T** — date-pair data doesn't suit area charts. Area fills imply sequential progression; T×T/Q×T lack monotonic relationships. -- **N×N, T×N, N×T** — purely nominal axes don't suit area charts. Area fills imply continuity/progression; nominal axes lack this. - -### Streamgraph matrix (6 tests) - -| # | x | y | color | n | what it tests | -|---|---|---|-------|---|---------------| -| 1 | T | Q | N(5) | 200 | 5 genres basic streamgraph | -| 2 | T | Q | N(10) | 800 | 10 industries large | -| 3 | T | Q | N(20) | 3000 | 20 series stress | -| 4 | T | Q | N(5) | 200 | 5 series ~20% sparse | -| 5 | O | Q | N(5) | 60 | Ordinal streamgraph | -| 6 | Q | Q | N(3) | 150 | Quant-x streamgraph | - -### Coverage summary - -Area Chart covers T×Q, O×Q, Q×O, Q×Q axis combos (17 tests). Streamgraph adds 6 tests exercising T×Q, O×Q, and Q×Q with multi-series color. Total **23 area/streamgraph tests**. - ---- - -## Grand total (matrix-driven chart tests) - -| Chart type | Tests | -|------------|-------| -| Scatter | 25 | -| Line | 16 | -| Bar | 19 | -| Stacked Bar | 12 | -| Grouped Bar | 12 | -| Area | 17 | -| Streamgraph | 6 | -| **Matrix subtotal** | **107** | - -Plus additional non-matrix test generators: -- Distribution charts (Histogram, Boxplot, Density, Strip) -- Specialized charts (Pie, Heatmap, Lollipop, Candlestick, Waterfall, etc.) -- Semantic context (39 tests) -- Facets (9 generators) -- Stress/sizing (4 generators) -- Temporal (7 generators) -- ECharts backend (24 generators) -- Chart.js backend (11 generators) - -**43 named test generators** total across all categories. diff --git a/site/src/shared/docs-catalog.ts b/site/src/shared/docs-catalog.ts index 0c38e407..51099f43 100644 --- a/site/src/shared/docs-catalog.ts +++ b/site/src/shared/docs-catalog.ts @@ -149,6 +149,12 @@ export const DOCUMENTATION_GROUPS: DocGroup[] = [ description: 'Monorepo setup, daily commands, and test strategy.', file: '../../../docs/DEVELOPMENT.md', }, + { + slug: 'test-plan', + title: 'Chart engine test plan', + description: 'Shared visual cases, coverage matrices, and backend bring-up workflow.', + file: '../../../docs/test_plan.md', + }, { slug: 'adding-a-semantic-type', title: 'Extending semantic types', From c73d597d46d999363a40983513c4eeda61806ee0 Mon Sep 17 00:00:00 2001 From: Chenglong Wang Date: Tue, 28 Jul 2026 17:43:08 -0700 Subject: [PATCH 007/164] theme research --- docs/reference-echarts.md | 1 + docs/reference-plotly.md | 3 +- docs/reference-vegalite.md | 1 + docs/zh-CN/reference-plotly.md | 3 +- .../flint-js/src/echarts/instantiate-spec.ts | 33 +- .../flint-js/src/echarts/templates/boxplot.ts | 141 ++- .../flint-js/src/plotly/templates/boxplot.ts | 36 +- .../flint-js/src/vegalite/templates/area.ts | 4 +- .../flint-js/src/vegalite/templates/bar.ts | 3 +- .../src/vegalite/templates/candlestick.ts | 52 +- .../src/vegalite/templates/scatter.ts | 176 ++- .../flint-js/src/vegalite/templates/utils.ts | 54 + .../tests/boxplot-grouped-dodge.test.ts | 2 +- .../flint-py/flint/vegalite/templates/area.py | 4 +- .../flint-py/flint/vegalite/templates/bar.py | 2 + .../flint/vegalite/templates/candlestick.py | 55 +- .../flint/vegalite/templates/utils.py | 58 + site/src/components/VegaLiteView.tsx | 6 +- site/src/main.tsx | 2 + site/src/playground/PlaygroundShell.tsx | 1 + site/src/playground/ThemeLab.tsx | 883 +++++++++++++++ site/src/playground/new-case-preview-data.ts | 303 ++++- .../theme-lab-assets/_flint-index.json | 474 ++++++++ .../theme-lab-assets/_headlines.json | 188 ++++ .../playground/theme-lab-assets/_themes.json | 101 ++ .../theme-lab-assets/_themespecs.json | 1000 +++++++++++++++++ .../theme-lab-assets/anscombe.flint.json | 331 ++++++ .../theme-lab-assets/auto-mpg.flint.json | 186 +++ .../theme-lab-assets/auto-mpg.nature.json | 136 +++ .../theme-lab-assets/big-mac.economist.json | 116 ++ .../theme-lab-assets/big-mac.flint.json | 103 ++ .../browser-pie.datawrapper.json | 172 +++ .../browser-pie.economist.json | 122 ++ .../theme-lab-assets/browser-pie.flint.json | 71 ++ .../browser-pie.mckinsey.json | 109 ++ .../theme-lab-assets/browser-pie.nature.json | 120 ++ .../theme-lab-assets/browser-pie.nyt.json | 110 ++ .../theme-lab-assets/browser-pie.powerbi.json | 119 ++ .../causes-death.datawrapper.json | 136 +++ .../causes-death.economist.json | 105 ++ .../theme-lab-assets/causes-death.flint.json | 95 ++ .../causes-death.mckinsey.json | 103 ++ .../theme-lab-assets/causes-death.nature.json | 99 ++ .../theme-lab-assets/causes-death.nyt.json | 91 ++ .../causes-death.powerbi.json | 106 ++ .../theme-lab-assets/cities-map.flint.json | 147 +++ .../co2-lollipop.datawrapper.json | 144 +++ .../theme-lab-assets/co2-lollipop.flint.json | 138 +++ .../theme-lab-assets/diamonds.flint.json | 134 +++ .../theme-lab-assets/driving.flint.json | 343 ++++++ .../theme-lab-assets/driving.nyt.json | 185 +++ .../earnings-education.flint.json | 119 ++ .../earnings-education.mckinsey.json | 115 ++ .../electricity-mix-area.economist.json | 149 +++ .../electricity-mix-area.flint.json | 188 ++++ .../electricity-stacked.flint.json | 140 +++ .../ev-share.datawrapper.json | 136 +++ .../theme-lab-assets/ev-share.economist.json | 131 +++ .../theme-lab-assets/ev-share.flint.json | 139 +++ .../theme-lab-assets/ev-share.mckinsey.json | 138 +++ .../theme-lab-assets/ev-share.nature.json | 147 +++ .../theme-lab-assets/ev-share.nyt.json | 116 ++ .../theme-lab-assets/ev-share.powerbi.json | 121 ++ .../theme-lab-assets/exam-ecdf.flint.json | 190 ++++ .../theme-lab-assets/exam-ecdf.nature.json | 129 +++ .../faithful-density.flint.json | 162 +++ .../theme-lab-assets/faithful-hist.flint.json | 156 +++ .../faithful-hist.nature.json | 119 ++ .../theme-lab-assets/faithful.flint.json | 198 ++++ .../fed-funds-step.flint.json | 94 ++ .../fed-funds-step.powerbi.json | 128 +++ .../gapminder-bubble.economist.json | 165 +++ .../gapminder-bubble.flint.json | 184 +++ .../theme-lab-assets/gdp-bartable.flint.json | 284 +++++ .../gdp-bartable.mckinsey.json | 95 ++ .../theme-lab-assets/happiness.flint.json | 116 ++ .../internet-users.flint.json | 81 ++ .../theme-lab-assets/iris-strip.flint.json | 193 ++++ .../theme-lab-assets/keeling.flint.json | 107 ++ .../theme-lab-assets/keeling.nyt.json | 107 ++ .../theme-lab-assets/kpi-sparkline.flint.json | 635 +++++++++++ .../kpi-sparkline.powerbi.json | 152 +++ .../life-expectancy.economist.json | 139 +++ .../life-expectancy.flint.json | 149 +++ .../lifeexp-dumbbell.flint.json | 142 +++ .../lifeexp-dumbbell.mckinsey.json | 157 +++ .../theme-lab-assets/marathon-wr.flint.json | 112 ++ .../medals-grouped.flint.json | 141 +++ .../theme-lab-assets/mobile-donut.flint.json | 60 + .../nutrition-radar.flint.json | 979 ++++++++++++++++ .../theme-lab-assets/oecd-facet-16.flint.json | 465 ++++++++ .../oecd-facet-16.powerbi.json | 193 ++++ .../oecd-unemployment-facet.economist.json | 138 +++ .../oecd-unemployment-facet.flint.json | 184 +++ .../theme-lab-assets/olympic-bump.flint.json | 145 +++ .../theme-lab-assets/olympic-bump.nyt.json | 134 +++ .../theme-lab-assets/penguins-box.flint.json | 190 ++++ .../theme-lab-assets/penguins-box.nature.json | 165 +++ .../penguins-violin.flint.json | 228 ++++ .../penguins-violin.nature.json | 195 ++++ .../theme-lab-assets/penguins.flint.json | 230 ++++ .../theme-lab-assets/penguins.nature.json | 145 +++ .../population-region.datawrapper.json | 153 +++ .../population-region.flint.json | 191 ++++ .../population-stream.flint.json | 190 ++++ .../population-stream.nyt.json | 141 +++ .../population-waterfall.flint.json | 197 ++++ .../population-waterfall.mckinsey.json | 127 +++ .../theme-lab-assets/population.flint.json | 95 ++ .../theme-lab-assets/population.mckinsey.json | 109 ++ .../theme-lab-assets/release-gantt.flint.json | 90 ++ .../renewable-bullet.flint.json | 285 +++++ .../renewable-bullet.powerbi.json | 118 ++ .../theme-lab-assets/renewable-kpi.flint.json | 208 ++++ .../renewable-kpi.powerbi.json | 191 ++++ .../renewables-projection.flint.json | 101 ++ .../renewables-projection.nyt.json | 154 +++ .../seattle-range.economist.json | 160 +++ .../theme-lab-assets/seattle-range.flint.json | 138 +++ .../theme-lab-assets/seattle-rose.flint.json | 172 +++ .../spending-quintile.flint.json | 196 ++++ .../spending-quintile.mckinsey.json | 144 +++ .../state-jobless.economist.json | 148 +++ .../theme-lab-assets/state-jobless.flint.json | 258 +++++ .../state-unemployment.datawrapper.json | 380 +++++++ .../state-unemployment.flint.json | 332 ++++++ .../theme-lab-assets/stock-candle.flint.json | 171 +++ .../stock-candle.powerbi.json | 143 +++ .../theme-lab-assets/sunspots.flint.json | 144 +++ .../theme-lab-assets/temp-anomaly.flint.json | 108 ++ .../theme-lab-assets/temp-anomaly.nyt.json | 110 ++ .../temp-heatmap.datawrapper.json | 396 +++++++ .../temp-heatmap.economist.json | 391 +++++++ .../theme-lab-assets/temp-heatmap.flint.json | 318 ++++++ .../temp-heatmap.mckinsey.json | 378 +++++++ .../theme-lab-assets/temp-heatmap.nature.json | 387 +++++++ .../theme-lab-assets/temp-heatmap.nyt.json | 397 +++++++ .../temp-heatmap.powerbi.json | 165 +++ .../temp-uncertainty.flint.json | 121 ++ .../temp-uncertainty.nature.json | 100 ++ .../theme-lab-assets/titanic.flint.json | 99 ++ .../trust-likert.datawrapper.json | 149 +++ .../theme-lab-assets/trust-likert.flint.json | 170 +++ .../us-pyramid.datawrapper.json | 178 +++ .../theme-lab-assets/us-pyramid.flint.json | 184 +++ .../us-unemployment.flint.json | 147 +++ .../theme-lab-assets/us-unemployment.nyt.json | 166 +++ 147 files changed, 24927 insertions(+), 105 deletions(-) create mode 100644 site/src/playground/ThemeLab.tsx create mode 100644 site/src/playground/theme-lab-assets/_flint-index.json create mode 100644 site/src/playground/theme-lab-assets/_headlines.json create mode 100644 site/src/playground/theme-lab-assets/_themes.json create mode 100644 site/src/playground/theme-lab-assets/_themespecs.json create mode 100644 site/src/playground/theme-lab-assets/anscombe.flint.json create mode 100644 site/src/playground/theme-lab-assets/auto-mpg.flint.json create mode 100644 site/src/playground/theme-lab-assets/auto-mpg.nature.json create mode 100644 site/src/playground/theme-lab-assets/big-mac.economist.json create mode 100644 site/src/playground/theme-lab-assets/big-mac.flint.json create mode 100644 site/src/playground/theme-lab-assets/browser-pie.datawrapper.json create mode 100644 site/src/playground/theme-lab-assets/browser-pie.economist.json create mode 100644 site/src/playground/theme-lab-assets/browser-pie.flint.json create mode 100644 site/src/playground/theme-lab-assets/browser-pie.mckinsey.json create mode 100644 site/src/playground/theme-lab-assets/browser-pie.nature.json create mode 100644 site/src/playground/theme-lab-assets/browser-pie.nyt.json create mode 100644 site/src/playground/theme-lab-assets/browser-pie.powerbi.json create mode 100644 site/src/playground/theme-lab-assets/causes-death.datawrapper.json create mode 100644 site/src/playground/theme-lab-assets/causes-death.economist.json create mode 100644 site/src/playground/theme-lab-assets/causes-death.flint.json create mode 100644 site/src/playground/theme-lab-assets/causes-death.mckinsey.json create mode 100644 site/src/playground/theme-lab-assets/causes-death.nature.json create mode 100644 site/src/playground/theme-lab-assets/causes-death.nyt.json create mode 100644 site/src/playground/theme-lab-assets/causes-death.powerbi.json create mode 100644 site/src/playground/theme-lab-assets/cities-map.flint.json create mode 100644 site/src/playground/theme-lab-assets/co2-lollipop.datawrapper.json create mode 100644 site/src/playground/theme-lab-assets/co2-lollipop.flint.json create mode 100644 site/src/playground/theme-lab-assets/diamonds.flint.json create mode 100644 site/src/playground/theme-lab-assets/driving.flint.json create mode 100644 site/src/playground/theme-lab-assets/driving.nyt.json create mode 100644 site/src/playground/theme-lab-assets/earnings-education.flint.json create mode 100644 site/src/playground/theme-lab-assets/earnings-education.mckinsey.json create mode 100644 site/src/playground/theme-lab-assets/electricity-mix-area.economist.json create mode 100644 site/src/playground/theme-lab-assets/electricity-mix-area.flint.json create mode 100644 site/src/playground/theme-lab-assets/electricity-stacked.flint.json create mode 100644 site/src/playground/theme-lab-assets/ev-share.datawrapper.json create mode 100644 site/src/playground/theme-lab-assets/ev-share.economist.json create mode 100644 site/src/playground/theme-lab-assets/ev-share.flint.json create mode 100644 site/src/playground/theme-lab-assets/ev-share.mckinsey.json create mode 100644 site/src/playground/theme-lab-assets/ev-share.nature.json create mode 100644 site/src/playground/theme-lab-assets/ev-share.nyt.json create mode 100644 site/src/playground/theme-lab-assets/ev-share.powerbi.json create mode 100644 site/src/playground/theme-lab-assets/exam-ecdf.flint.json create mode 100644 site/src/playground/theme-lab-assets/exam-ecdf.nature.json create mode 100644 site/src/playground/theme-lab-assets/faithful-density.flint.json create mode 100644 site/src/playground/theme-lab-assets/faithful-hist.flint.json create mode 100644 site/src/playground/theme-lab-assets/faithful-hist.nature.json create mode 100644 site/src/playground/theme-lab-assets/faithful.flint.json create mode 100644 site/src/playground/theme-lab-assets/fed-funds-step.flint.json create mode 100644 site/src/playground/theme-lab-assets/fed-funds-step.powerbi.json create mode 100644 site/src/playground/theme-lab-assets/gapminder-bubble.economist.json create mode 100644 site/src/playground/theme-lab-assets/gapminder-bubble.flint.json create mode 100644 site/src/playground/theme-lab-assets/gdp-bartable.flint.json create mode 100644 site/src/playground/theme-lab-assets/gdp-bartable.mckinsey.json create mode 100644 site/src/playground/theme-lab-assets/happiness.flint.json create mode 100644 site/src/playground/theme-lab-assets/internet-users.flint.json create mode 100644 site/src/playground/theme-lab-assets/iris-strip.flint.json create mode 100644 site/src/playground/theme-lab-assets/keeling.flint.json create mode 100644 site/src/playground/theme-lab-assets/keeling.nyt.json create mode 100644 site/src/playground/theme-lab-assets/kpi-sparkline.flint.json create mode 100644 site/src/playground/theme-lab-assets/kpi-sparkline.powerbi.json create mode 100644 site/src/playground/theme-lab-assets/life-expectancy.economist.json create mode 100644 site/src/playground/theme-lab-assets/life-expectancy.flint.json create mode 100644 site/src/playground/theme-lab-assets/lifeexp-dumbbell.flint.json create mode 100644 site/src/playground/theme-lab-assets/lifeexp-dumbbell.mckinsey.json create mode 100644 site/src/playground/theme-lab-assets/marathon-wr.flint.json create mode 100644 site/src/playground/theme-lab-assets/medals-grouped.flint.json create mode 100644 site/src/playground/theme-lab-assets/mobile-donut.flint.json create mode 100644 site/src/playground/theme-lab-assets/nutrition-radar.flint.json create mode 100644 site/src/playground/theme-lab-assets/oecd-facet-16.flint.json create mode 100644 site/src/playground/theme-lab-assets/oecd-facet-16.powerbi.json create mode 100644 site/src/playground/theme-lab-assets/oecd-unemployment-facet.economist.json create mode 100644 site/src/playground/theme-lab-assets/oecd-unemployment-facet.flint.json create mode 100644 site/src/playground/theme-lab-assets/olympic-bump.flint.json create mode 100644 site/src/playground/theme-lab-assets/olympic-bump.nyt.json create mode 100644 site/src/playground/theme-lab-assets/penguins-box.flint.json create mode 100644 site/src/playground/theme-lab-assets/penguins-box.nature.json create mode 100644 site/src/playground/theme-lab-assets/penguins-violin.flint.json create mode 100644 site/src/playground/theme-lab-assets/penguins-violin.nature.json create mode 100644 site/src/playground/theme-lab-assets/penguins.flint.json create mode 100644 site/src/playground/theme-lab-assets/penguins.nature.json create mode 100644 site/src/playground/theme-lab-assets/population-region.datawrapper.json create mode 100644 site/src/playground/theme-lab-assets/population-region.flint.json create mode 100644 site/src/playground/theme-lab-assets/population-stream.flint.json create mode 100644 site/src/playground/theme-lab-assets/population-stream.nyt.json create mode 100644 site/src/playground/theme-lab-assets/population-waterfall.flint.json create mode 100644 site/src/playground/theme-lab-assets/population-waterfall.mckinsey.json create mode 100644 site/src/playground/theme-lab-assets/population.flint.json create mode 100644 site/src/playground/theme-lab-assets/population.mckinsey.json create mode 100644 site/src/playground/theme-lab-assets/release-gantt.flint.json create mode 100644 site/src/playground/theme-lab-assets/renewable-bullet.flint.json create mode 100644 site/src/playground/theme-lab-assets/renewable-bullet.powerbi.json create mode 100644 site/src/playground/theme-lab-assets/renewable-kpi.flint.json create mode 100644 site/src/playground/theme-lab-assets/renewable-kpi.powerbi.json create mode 100644 site/src/playground/theme-lab-assets/renewables-projection.flint.json create mode 100644 site/src/playground/theme-lab-assets/renewables-projection.nyt.json create mode 100644 site/src/playground/theme-lab-assets/seattle-range.economist.json create mode 100644 site/src/playground/theme-lab-assets/seattle-range.flint.json create mode 100644 site/src/playground/theme-lab-assets/seattle-rose.flint.json create mode 100644 site/src/playground/theme-lab-assets/spending-quintile.flint.json create mode 100644 site/src/playground/theme-lab-assets/spending-quintile.mckinsey.json create mode 100644 site/src/playground/theme-lab-assets/state-jobless.economist.json create mode 100644 site/src/playground/theme-lab-assets/state-jobless.flint.json create mode 100644 site/src/playground/theme-lab-assets/state-unemployment.datawrapper.json create mode 100644 site/src/playground/theme-lab-assets/state-unemployment.flint.json create mode 100644 site/src/playground/theme-lab-assets/stock-candle.flint.json create mode 100644 site/src/playground/theme-lab-assets/stock-candle.powerbi.json create mode 100644 site/src/playground/theme-lab-assets/sunspots.flint.json create mode 100644 site/src/playground/theme-lab-assets/temp-anomaly.flint.json create mode 100644 site/src/playground/theme-lab-assets/temp-anomaly.nyt.json create mode 100644 site/src/playground/theme-lab-assets/temp-heatmap.datawrapper.json create mode 100644 site/src/playground/theme-lab-assets/temp-heatmap.economist.json create mode 100644 site/src/playground/theme-lab-assets/temp-heatmap.flint.json create mode 100644 site/src/playground/theme-lab-assets/temp-heatmap.mckinsey.json create mode 100644 site/src/playground/theme-lab-assets/temp-heatmap.nature.json create mode 100644 site/src/playground/theme-lab-assets/temp-heatmap.nyt.json create mode 100644 site/src/playground/theme-lab-assets/temp-heatmap.powerbi.json create mode 100644 site/src/playground/theme-lab-assets/temp-uncertainty.flint.json create mode 100644 site/src/playground/theme-lab-assets/temp-uncertainty.nature.json create mode 100644 site/src/playground/theme-lab-assets/titanic.flint.json create mode 100644 site/src/playground/theme-lab-assets/trust-likert.datawrapper.json create mode 100644 site/src/playground/theme-lab-assets/trust-likert.flint.json create mode 100644 site/src/playground/theme-lab-assets/us-pyramid.datawrapper.json create mode 100644 site/src/playground/theme-lab-assets/us-pyramid.flint.json create mode 100644 site/src/playground/theme-lab-assets/us-unemployment.flint.json create mode 100644 site/src/playground/theme-lab-assets/us-unemployment.nyt.json diff --git a/docs/reference-echarts.md b/docs/reference-echarts.md index 4ee28465..cd730d55 100644 --- a/docs/reference-echarts.md +++ b/docs/reference-echarts.md @@ -65,6 +65,7 @@ _No template-specific parameters._ | Parameter | Control | Domain | Default | Availability | Description | |---|---|---|---|---|---| | `whiskerMethod` | choice | `iqr` (Tukey (1.5 × IQR)), `minmax` (Min–Max) | `iqr` | always | Whiskers | +| `showPoints` | toggle | on / off | `false` | conditional | Overlay point markers on the line. | | `showOutliers` | toggle | on / off | `true` | conditional | Outliers | | `dodge` | choice | `auto` (Auto), `local` (Local (compact)), `global` (Global (aligned)) | `auto` | conditional | Dodge | diff --git a/docs/reference-plotly.md b/docs/reference-plotly.md index 02fc7478..9e31d1ee 100644 --- a/docs/reference-plotly.md +++ b/docs/reference-plotly.md @@ -128,7 +128,8 @@ _No template-specific parameters._ | Parameter | Control | Domain | Default | Availability | Description | |---|---|---|---|---|---| -| `showOutliers` | toggle | on / off | `true` | always | Outliers | +| `showPoints` | toggle | on / off | `false` | conditional | Overlay point markers on the line. | +| `showOutliers` | toggle | on / off | `true` | conditional | Outliers | ### ![](chart-icon-violin.svg) Violin Plot diff --git a/docs/reference-vegalite.md b/docs/reference-vegalite.md index c463217c..264a40fb 100644 --- a/docs/reference-vegalite.md +++ b/docs/reference-vegalite.md @@ -219,6 +219,7 @@ The **Availability** column shows whether a parameter is `always` available or ` | Parameter | Control | Domain | Default | Availability | Description | |---|---|---|---|---|---| | `whiskerMethod` | choice | `iqr` (Tukey (1.5 × IQR)), `minmax` (Min–Max) | `iqr` | always | Whiskers | +| `showPoints` | toggle | on / off | `false` | conditional | Overlay point markers on the line. | | `showOutliers` | toggle | on / off | `true` | conditional | Outliers | | `dodge` | choice | `auto` (Auto), `local` (Local (compact)), `global` (Global (aligned)) | `auto` | conditional | Dodge | | `independentYAxis` | toggle | on / off | `false` | conditional | Use independent y-scales for facets. | diff --git a/docs/zh-CN/reference-plotly.md b/docs/zh-CN/reference-plotly.md index ecf7032c..14bc8748 100644 --- a/docs/zh-CN/reference-plotly.md +++ b/docs/zh-CN/reference-plotly.md @@ -124,7 +124,8 @@ _无模板专用参数。_ | 参数 | 控件 | 取值范围 | 默认值 | 可用性 | 说明 | |---|---|---|---|---|---| -| `showOutliers` | toggle | on / off | `true` | always | 显示离群点。 | +| `showPoints` | toggle | on / off | `false` | conditional | 在线上叠加点标记。 | +| `showOutliers` | toggle | on / off | `true` | conditional | 显示离群点。 | ### ![](chart-icon-violin.svg) Violin Plot diff --git a/packages/flint-js/src/echarts/instantiate-spec.ts b/packages/flint-js/src/echarts/instantiate-spec.ts index 6329c0d3..b4474d4c 100644 --- a/packages/flint-js/src/echarts/instantiate-spec.ts +++ b/packages/flint-js/src/echarts/instantiate-spec.ts @@ -257,6 +257,15 @@ function pyramidNiceTickStep(niceMax: number): number { return niceMax / 4; } +/** + * Overlay companions: the scatter series a boxplot draws alongside itself for + * its outliers or for the full raw sample. They are separate series only + * because ECharts needs a different series type to draw them, so they must + * take the colour of the box they sit on rather than the next palette slot. + */ +const COMPANION_SUFFIX = / \((?:outliers|points)\)$/; +const isCompanionSeries = (s: any): boolean => s?._companion === true; + /** * Grouped boxplot needs enough per-category horizontal room; otherwise boxes overlap. * Return a conservative minimum plot width for category x-axis grouped boxplots. @@ -1008,8 +1017,7 @@ export function ecApplyLayoutToSpec( // 当存在调色板时,覆盖模板中的硬编码 itemStyle.color, // 让最终颜色真正由 colorDecisions / colormap 注册表驱动。 - if (effectivePalette && effectivePalette.length > 0 && Array.isArray(option.series)) { - const palette_ = effectivePalette; // local const so TS narrows inside closures + if (effectivePalette && effectivePalette.length > 0 && Array.isArray(option.series)) { const palette_ = effectivePalette; // local const so TS narrows inside closures const n = palette_.length; const schemeType = colorDecision?.schemeType; @@ -1082,13 +1090,13 @@ export function ecApplyLayoutToSpec( option.series.forEach((s: any, idx: number) => { if (!s) return; const rawName: string = typeof s.name === 'string' ? s.name : (s.name != null ? String(s.name) : ''); - const baseName = rawName.endsWith(' (outliers)') - ? rawName.slice(0, -' (outliers)'.length) - : rawName; + const baseName = rawName.replace(COMPANION_SUFFIX, ''); const mappedColor = baseName && categoryToColor.has(baseName) ? categoryToColor.get(baseName) : palette_[idx % n]; s.itemStyle = s.itemStyle || {}; + // A hollow boxplot (raw sample overlaid) declares a per-datum + // transparent fill; the palette still owns its outline. s.itemStyle.color = mappedColor!; if (s.type === 'boxplot') { s.itemStyle.borderColor = mappedColor!; @@ -1225,7 +1233,9 @@ export function ecApplyLayoutToSpec( : new Map(); // 只对「需要上色」的 series 从 palette 取色,已设 color 的(如连接线、参考线)不占下标,使 Min/Max 等得到第 1、2 个颜色 - const colorableCount = option.series.filter((s: any) => s && s.itemStyle?.color == null).length; + const colorableCount = option.series.filter( + (s: any) => s && s.itemStyle?.color == null && !isCompanionSeries(s), + ).length; const spacedIndices = useEvenSpacing && colorableCount > 0 ? pickEvenlySpacedColorIndices(n, colorableCount) : null; @@ -1236,6 +1246,17 @@ export function ecApplyLayoutToSpec( s.itemStyle = s.itemStyle || {}; if (s.itemStyle.color != null) return; + // An overlay companion (outliers / the raw sample) belongs to + // the series it sits on, so it inherits that colour instead + // of consuming a palette slot and landing a different hue. + if (isCompanionSeries(s)) { + const owner = option.series[idx - 1]; + if (owner?.itemStyle?.color != null) { + s.itemStyle.color = owner.itemStyle.color; + return; + } + } + // Rank / Index 颜色映射:根据 rank 数值在连续色带上取色 if (isRankLikeColor && rankLegendColorMap.size > 0) { const rawName: string = typeof s.name === 'string' diff --git a/packages/flint-js/src/echarts/templates/boxplot.ts b/packages/flint-js/src/echarts/templates/boxplot.ts index 91e3a438..3a2cee21 100644 --- a/packages/flint-js/src/echarts/templates/boxplot.ts +++ b/packages/flint-js/src/echarts/templates/boxplot.ts @@ -79,19 +79,44 @@ function findOutliers(values: number[]): number[] { return values.filter(v => v < lo || v > hi); } -function boxplotLaneOffset(bandWidth: number, laneCount: number, laneIndex: number): number { +/** Lane centre offset and box width, in pixels, within one category band. */ +function boxplotLaneGeometry(bandWidth: number, laneCount: number, laneIndex: number) { const availableWidth = bandWidth * 0.8 - 2; const boxGap = availableWidth / laneCount * 0.3; const boxWidth = (availableWidth - boxGap * (laneCount - 1)) / laneCount; - return boxWidth / 2 - availableWidth / 2 + laneIndex * (boxGap + boxWidth); + return { + offset: boxWidth / 2 - availableWidth / 2 + laneIndex * (boxGap + boxWidth), + boxWidth, + }; +} + +// Half-width of the raw-observation jitter cloud, as a fraction of one box. +const POINT_JITTER_FRACTION = 0.35; + +/** + * Deterministic jitter in [-1, 1]. A golden-ratio sequence spreads successive + * points evenly instead of clumping the way independent random draws do, and + * being deterministic keeps a re-render pixel-identical. + */ +function jitterAt(index: number): number { + return ((index * 0.6180339887498949) % 1) * 2 - 1; } -function makeOutlierSeries( +/** + * Scatter overlay drawn on top of the boxes — either just the outliers, or + * every raw observation when `showPoints` is on. + * + * ECharts has no per-point band offset, so this is a `custom` series that + * resolves the lane (and the jitter carried as the datum's third element) + * against the live band width at render time. + */ +function makePointSeries( name: string, data: any[], laneIndex: number, laneCount: number, horizontal: boolean, + fullSample = false, ): any { return { name, @@ -100,23 +125,35 @@ function makeOutlierSeries( data, encode: { tooltip: [0, 1] }, z: 3, + // Marks this as an overlay belonging to the boxplot series before it, so + // palette assignment gives it that box's colour instead of a new slot. + _companion: true, renderItem: (_params: any, api: any) => { const category = Number(api.value(0)); const value = Number(api.value(1)); + const jitter = Number(api.value(2)) || 0; const point = horizontal ? api.coord([value, category]) : api.coord([category, value]); const size = api.size(horizontal ? [0, 1] : [1, 0]); const bandWidth = Math.abs(horizontal ? size[1] : size[0]); - const offset = boxplotLaneOffset(bandWidth, laneCount, laneIndex); + const lane = boxplotLaneGeometry(bandWidth, laneCount, laneIndex); + const offset = lane.offset + jitter * lane.boxWidth * POINT_JITTER_FRACTION; + const color = api.visual('color'); return { type: 'circle', shape: { cx: point[0] + (horizontal ? 0 : offset), cy: point[1] + (horizontal ? offset : 0), - r: 2, + r: fullSample ? Math.max(1.6, Math.min(3.2, lane.boxWidth * 0.08)) : 2, }, - style: { fill: api.visual('color') }, + // With the whole sample drawn the box goes hollow and the points + // become the mark that spends saturated ink, so they carry the + // group colour. Slight transparency lets dense regions read as + // density; the hairline halo keeps overlaps countable. + style: fullSample + ? { fill: color, opacity: 0.7, stroke: '#ffffff', lineWidth: 0.5 } + : { fill: color }, }; }, }; @@ -187,8 +224,11 @@ export const ecBoxplotDef: ChartTemplateDef = { // whiskers, outliers are drawn as a scatter overlay unless suppressed. const whiskerMethod: 'iqr' | 'minmax' = ctx.chartProperties?.whiskerMethod === 'minmax' ? 'minmax' : 'iqr'; + // `showPoints` overlays every raw observation instead — which makes the + // separate outlier marks redundant, since they are drawn too. + const showPoints = ctx.chartProperties?.showPoints === true; const showOutliers = - whiskerMethod === 'iqr' && ctx.chartProperties?.showOutliers !== false; + !showPoints && whiskerMethod === 'iqr' && ctx.chartProperties?.showOutliers !== false; // Determine which axis is categorical and which is quantitative const xIsDiscrete = isDiscrete(xCS.type); @@ -274,7 +314,7 @@ export const ecBoxplotDef: ChartTemplateDef = { const catGroups = groupBy(table, catField); for (let lane = 0; lane < maxPerBand; lane++) { const boxData: ({ value: [number, number, number, number, number]; itemStyle: any } | '-')[] = []; - const outlierData: any[] = []; + const pointData: any[] = []; for (let i = 0; i < categories.length; i++) { const cat = categories[i]; const g = perBand.get(cat)?.[lane]; @@ -283,15 +323,24 @@ export const ecBoxplotDef: ChartTemplateDef = { const values = rows.map((r: any) => Number(r[valField])).filter((v: number) => isFinite(v)); if (!values.length) { boxData.push('-'); continue; } const c = colorFor(g); - boxData.push({ value: fiveNumberSummary(values, whiskerMethod), itemStyle: { color: c, borderColor: c } }); - if (showOutliers) { - for (const o of findOutliers(values)) outlierData.push({ value: [i, o], itemStyle: { color: c } }); + boxData.push({ + value: fiveNumberSummary(values, whiskerMethod), + // Hollow box when the sample is drawn (see makePointSeries). + itemStyle: { color: showPoints ? 'transparent' : c, borderColor: c }, + }); + if (showPoints) { + values.forEach((v, k) => pointData.push([i, v, jitterAt(k)])); + } else if (showOutliers) { + for (const o of findOutliers(values)) pointData.push({ value: [i, o], itemStyle: { color: c } }); } } - option.series.push({ name: `__lane${lane}`, type: 'boxplot', data: boxData }); - if (outlierData.length > 0) { - option.series.push(makeOutlierSeries( - `__lane${lane} (outliers)`, outlierData, lane, maxPerBand, isHorizontal, + option.series.push({ + name: `__lane${lane}`, type: 'boxplot', data: boxData, + ...(showPoints ? { itemStyle: { borderWidth: 1.5 } } : {}), + }); + if (pointData.length > 0) { + option.series.push(makePointSeries( + `__lane${lane} (points)`, pointData, lane, maxPerBand, isHorizontal, showPoints, )); } } @@ -305,8 +354,8 @@ export const ecBoxplotDef: ChartTemplateDef = { for (let cIdx = 0; cIdx < colorCategories.length; cIdx++) { const colorName = colorCategories[cIdx]; - const boxData: ([number, number, number, number, number] | '-')[] = []; - const outlierData: [number, number][] = []; + const boxData: any[] = []; + const pointData: number[][] = []; for (let i = 0; i < categories.length; i++) { const cat = categories[i]; @@ -319,11 +368,21 @@ export const ecBoxplotDef: ChartTemplateDef = { // flat box at 0 in every unoccupied lane (the sparse-dodge // zero-box bug). ECharts boxplot does not accept `null` // data items; `'-'` is its missing-value sentinel. - boxData.push(values.length ? fiveNumberSummary(values, whiskerMethod) : '-'); + // A per-datum transparent fill hollows the box when the raw + // sample is drawn; the palette still owns the outline, which + // `instantiate-spec` assigns at series level. + boxData.push( + !values.length ? '-' + : showPoints + ? { value: fiveNumberSummary(values, whiskerMethod), itemStyle: { color: 'transparent' } } + : fiveNumberSummary(values, whiskerMethod), + ); - if (showOutliers) { + if (showPoints) { + values.forEach((v, k) => pointData.push([i, v, jitterAt(k)])); + } else if (showOutliers) { for (const o of findOutliers(values)) { - outlierData.push([i, o]); + pointData.push([i, o]); } } } @@ -332,11 +391,12 @@ export const ecBoxplotDef: ChartTemplateDef = { name: colorName, type: 'boxplot', data: boxData, + ...(showPoints ? { itemStyle: { borderWidth: 1.5 } } : {}), // itemStyle 由 ecApplyLayoutToSpec 按 colorDecisions 填充 }); - if (outlierData.length > 0) { - option.series.push(makeOutlierSeries( - colorName + ' (outliers)', outlierData, cIdx, colorCategories.length, isHorizontal, + if (pointData.length > 0) { + option.series.push(makePointSeries( + colorName + ' (points)', pointData, cIdx, colorCategories.length, isHorizontal, showPoints, )); } } @@ -346,18 +406,21 @@ export const ecBoxplotDef: ChartTemplateDef = { } else { // Single boxplot series (no color grouping) const catGroups = groupBy(table, catField); - const boxData: [number, number, number, number, number][] = []; - const outlierData: [number, number][] = []; + const boxData: any[] = []; + const pointData: number[][] = []; for (let i = 0; i < categories.length; i++) { const cat = categories[i]; const rows = catGroups.get(cat) || []; const values = rows.map((r: any) => Number(r[valField])).filter((v: number) => isFinite(v)); - boxData.push(fiveNumberSummary(values, whiskerMethod)); + const summary = fiveNumberSummary(values, whiskerMethod); + boxData.push(showPoints ? { value: summary, itemStyle: { color: 'transparent' } } : summary); - if (showOutliers) { + if (showPoints) { + values.forEach((v, k) => pointData.push([i, v, jitterAt(k)])); + } else if (showOutliers) { for (const o of findOutliers(values)) { - outlierData.push([i, o]); + pointData.push([i, o]); } } } @@ -365,10 +428,11 @@ export const ecBoxplotDef: ChartTemplateDef = { option.series.push({ type: 'boxplot', data: boxData, + ...(showPoints ? { itemStyle: { borderWidth: 1.5 } } : {}), // 单系列颜色由 ecApplyLayoutToSpec 使用 cat10[0] 等统一默认 }); - if (outlierData.length > 0) { - option.series.push(makeOutlierSeries('Outliers', outlierData, 0, 1, isHorizontal)); + if (pointData.length > 0) { + option.series.push(makePointSeries('Points', pointData, 0, 1, isHorizontal, showPoints)); } } @@ -385,9 +449,24 @@ export const ecBoxplotDef: ChartTemplateDef = { ], defaultValue: 'iqr', } as ChartPropertyDef, + { + key: 'showPoints', label: 'Points', type: 'binary', defaultValue: false, + // Jitter needs a band to scatter within, so this is only meaningful + // once one position axis is discrete. + check: (ctx) => ({ + applicable: isDiscrete(ctx.channelSemantics?.x?.type) + || isDiscrete(ctx.channelSemantics?.y?.type), + }), + } as ChartPropertyDef, { key: 'showOutliers', label: 'Outliers', type: 'binary', defaultValue: true, - check: (ctx) => ({ applicable: ctx.chartProperties?.whiskerMethod !== 'minmax' }), + // Outliers exist only with Tukey whiskers; min–max whiskers absorb + // every point. And once every observation is drawn, the outlier + // marks are just duplicates. + check: (ctx) => ({ + applicable: ctx.chartProperties?.whiskerMethod !== 'minmax' + && ctx.chartProperties?.showPoints !== true, + }), } as ChartPropertyDef, { key: 'dodge', label: 'Dodge', type: 'discrete', diff --git a/packages/flint-js/src/plotly/templates/boxplot.ts b/packages/flint-js/src/plotly/templates/boxplot.ts index dbd5904e..d070799f 100644 --- a/packages/flint-js/src/plotly/templates/boxplot.ts +++ b/packages/flint-js/src/plotly/templates/boxplot.ts @@ -55,20 +55,35 @@ export const plBoxplotDef: ChartTemplateDef = { const isHorizontal = catAxis === 'y'; const categories = extractCategories(table, catField, channelSemantics[catAxis]?.ordinalSortOrder); + // `showPoints` overlays every raw observation on the box; that makes the + // separate outlier marks redundant, since those points are drawn too. + const showPoints = chartProperties?.showPoints === true; const showOutliers = chartProperties?.showOutliers !== false; - const boxpoints = showOutliers ? 'outliers' : false; + const boxpoints = showPoints ? 'all' : (showOutliers ? 'outliers' : false); const palette = getPlotlyPalette(ctx, 'color'); const makeTrace = (name: string | undefined, rows: any[], colorIdx: number) => { const cats = rows.map((r: any) => String(r[catField] ?? '')); const vals = rows.map((r: any) => Number(r[valField])); + const seriesColor = getSeriesColor(palette, colorIdx); return { type: 'box', ...(name != null ? { name } : {}), ...(isHorizontal ? { y: cats, x: vals } : { x: cats, y: vals }), boxpoints, - marker: { color: getSeriesColor(palette, colorIdx), size: 3 }, - line: { color: getSeriesColor(palette, colorIdx) }, + // Drawing the sample inverts the visual hierarchy: the sample + // becomes the figure and the box demotes to scaffolding over it. + // So the box goes hollow and keeps only its outline, and the + // group colour lives on the points — fill *and* point colour + // would encode the group twice. `pointpos: 0` centres the cloud + // on the box instead of parking it alongside. + ...(showPoints + ? { jitter: 0.6, pointpos: 0, fillcolor: 'rgba(0,0,0,0)' } + : {}), + marker: showPoints + ? { color: seriesColor, size: 4, opacity: 0.7, line: { color: '#ffffff', width: 0.5 } } + : { color: seriesColor, size: 3 }, + line: { color: seriesColor, ...(showPoints ? { width: 1.5 } : {}) }, }; }; @@ -108,6 +123,19 @@ export const plBoxplotDef: ChartTemplateDef = { delete spec.encoding; }, properties: [ - { key: 'showOutliers', label: 'Outliers', type: 'binary', defaultValue: true } as ChartPropertyDef, + { + key: 'showPoints', label: 'Points', type: 'binary', defaultValue: false, + // Jitter needs a band to scatter within, so this is only meaningful + // once one position axis is discrete. + check: (ctx) => ({ + applicable: isDiscreteType(ctx.channelSemantics?.x?.type) + || isDiscreteType(ctx.channelSemantics?.y?.type), + }), + } as ChartPropertyDef, + { + key: 'showOutliers', label: 'Outliers', type: 'binary', defaultValue: true, + // Once every observation is drawn, the outlier marks are duplicates. + check: (ctx) => ({ applicable: ctx.chartProperties?.showPoints !== true }), + } as ChartPropertyDef, ], }; diff --git a/packages/flint-js/src/vegalite/templates/area.ts b/packages/flint-js/src/vegalite/templates/area.ts index 7bef3b80..38721a11 100644 --- a/packages/flint-js/src/vegalite/templates/area.ts +++ b/packages/flint-js/src/vegalite/templates/area.ts @@ -3,7 +3,7 @@ import { ChartTemplateDef, ChartPropertyDef } from '../../core/types'; import { makeCartesianPivot } from '../../core/pivot'; -import { defaultBuildEncodings, setMarkProp } from './utils'; +import { defaultBuildEncodings, setMarkProp, alignStackOrderToColorOrder } from './utils'; const interpolateConfigProperty: ChartPropertyDef = { key: "interpolate", label: "Curve", type: "discrete", options: [ @@ -147,6 +147,7 @@ export const areaChartDef: ChartTemplateDef = { } else if (config?.stackMode !== 'layered') { interpolateSparseStack(spec, ctx); } + alignStackOrderToColorOrder(spec, ctx); }, properties: [ interpolateConfigProperty, @@ -196,6 +197,7 @@ export const streamgraphDef: ChartTemplateDef = { // A streamgraph is always centre-stacked → interpolate sparse gaps so the // stack stays continuous (see interpolateSparseStack). interpolateSparseStack(spec, ctx); + alignStackOrderToColorOrder(spec, ctx); }, properties: [interpolateConfigProperty] as ChartPropertyDef[], }; diff --git a/packages/flint-js/src/vegalite/templates/bar.ts b/packages/flint-js/src/vegalite/templates/bar.ts index 35216767..ad761e12 100644 --- a/packages/flint-js/src/vegalite/templates/bar.ts +++ b/packages/flint-js/src/vegalite/templates/bar.ts @@ -11,7 +11,7 @@ import { } from '../../core/axis-detection'; import { defaultBuildEncodings, setMarkProp, adjustBarMarks, adjustRectTiling, - resolveAsDiscrete, + resolveAsDiscrete, alignStackOrderToColorOrder, } from './utils'; const HEATMAP_SCHEME_COLORS: Record = { @@ -366,6 +366,7 @@ export const stackedBarChartDef: ChartTemplateDef = { } } } + alignStackOrderToColorOrder(spec, ctx); adjustBarMarks(spec, ctx); }, properties: [ diff --git a/packages/flint-js/src/vegalite/templates/candlestick.ts b/packages/flint-js/src/vegalite/templates/candlestick.ts index 10493a5e..0fdb598d 100644 --- a/packages/flint-js/src/vegalite/templates/candlestick.ts +++ b/packages/flint-js/src/vegalite/templates/candlestick.ts @@ -2,6 +2,7 @@ // Licensed under the MIT License. import { ChartTemplateDef } from '../../core/types'; +import { adjustBarMarks } from './utils'; export const candlestickChartDef: ChartTemplateDef = { chart: "Candlestick Chart", @@ -44,28 +45,55 @@ export const candlestickChartDef: ChartTemplateDef = { if (close) spec.layer[1].encoding.y2 = { field: close.field }; if (open?.field && close?.field) { + // `<=`, not `<`: a session that closes exactly where it opened has + // not fallen, and colouring it as a decline is a false statement. spec.encoding.color = { condition: { - test: `datum['${open.field}'] < datum['${close.field}']`, + test: `datum['${open.field}'] <= datum['${close.field}']`, value: "#06982d", }, value: "#ae1325", }; } - // Compute bar width from x-axis cardinality - const table = ctx.table; - const plotWidth = ctx.canvasSize?.width || 400; - const xField = spec.encoding?.x?.field; - let barSize: number; - - if (xField && table?.length > 0) { - const cardinality = new Set(table.map((r: any) => r[xField])).size; - barSize = Math.max(2, Math.min(20, Math.round(plotWidth * 0.6 / cardinality))); + // Body width. + // + // On a banded *continuous* x — the usual case, dates — the slot width + // is set by the smallest gap between observations, not by the row + // count: nine trading days spanning eleven calendar days occupy eleven + // slots, two of which are the weekend. Sizing on cardinality makes + // every body wider than its own slot and adjacent candles fuse into a + // single polygon. adjustBarMarks() already performs that min-gap + // analysis for bar marks, so use it rather than keep a second, wrong + // copy of the arithmetic here. + // + // It returns the largest *non-overlapping* size, and bodies that + // merely touch still read as one shape when consecutive sessions move + // the same way. A candlestick needs a visible gutter, so take a + // fraction of the fitted width. + const BODY_FILL = 0.8; + if ((ctx.layout?.xContinuousAsDiscrete ?? 0) > 0) { + adjustBarMarks(spec, ctx); + const fitted = (spec.layer[1].mark as { size?: number })?.size ?? 14; + spec.layer[1].mark = { ...spec.layer[1].mark, size: Math.max(2, Math.floor(fitted * BODY_FILL)) }; } else { - barSize = 14; + const step = ctx.layout?.xStep ?? 20; + spec.layer[1].mark = { ...spec.layer[1].mark, size: Math.max(2, Math.round(step * BODY_FILL)) }; } - spec.layer[1].mark = { ...spec.layer[1].mark, size: barSize }; + // Doji sessions. + // + // When open === close the open→close bar has zero height and vanishes, + // so a flat session renders as a bare wick with no candle on it. Draw + // it as a horizontal tick at the shared price, which is the convention + // and is exactly what the bar degenerates to. + if (open?.field && close?.field) { + const bodySize = (spec.layer[1].mark as { size?: number })?.size ?? 14; + spec.layer.push({ + transform: [{ filter: `datum['${open.field}'] === datum['${close.field}']` }], + mark: { type: "tick", size: bodySize, thickness: 2 }, + encoding: { y: { field: close.field } }, + }); + } }, }; diff --git a/packages/flint-js/src/vegalite/templates/scatter.ts b/packages/flint-js/src/vegalite/templates/scatter.ts index 0f8d3fe1..8de92fd4 100644 --- a/packages/flint-js/src/vegalite/templates/scatter.ts +++ b/packages/flint-js/src/vegalite/templates/scatter.ts @@ -16,6 +16,13 @@ const isDiscreteType = (t: string | undefined) => t === 'nominal' || t === 'ordi // per-subgroup lane. The remainder becomes the gap between adjacent boxes. const BOXPLOT_BAND_FILL = 0.7; const GROUPED_BOXPLOT_LANE_FILL = 0.85; +// Half-width of the raw-observation jitter cloud, as a fraction of one lane. +// 0.3 spreads the points across the middle ~60% of the lane, so the cloud sits +// inside its box rather than spilling over the neighbouring one. +const POINT_JITTER_FRACTION = 0.3; +// Ink for the box skeleton once the box is hollow — dark and neutral, so the +// median reads as a summary statistic rather than as another category. +const SILHOUETTE_INK = '#2b2f36'; // Vega-Lite's default discrete position band scale reserves ~20% of each step as // inter-band padding, so only ~80% of the step is usable drawing width. Grouped // box sizing must use this usable width when splitting a band into sub-lanes, @@ -214,25 +221,52 @@ export const boxplotDef: ChartTemplateDef = { instantiate: (spec, ctx) => { defaultBuildEncodings(spec, ctx.resolvedEncodings); + const props = ctx.chartProperties; + const layout = ctx.layout; + const hasDiscreteX = layout.xNominalCount > 0; + const hasDiscreteAxis = hasDiscreteX || layout.yNominalCount > 0; + + // `showPoints` overlays every raw observation on top of the box. A box + // summarises a sample; at small n it can summarise almost nothing, so + // being able to show the sample is what makes the summary honest. + // Jitter needs a band to scatter within, hence the discrete-axis gate. + const showPoints = props?.showPoints === true && hasDiscreteAxis; + // Whisker convention + outlier visibility (design choices, not styling). // whiskerMethod 'minmax' → whiskers span the full data range; VL draws // no outlier points (they are inside the whiskers by definition). // whiskerMethod 'iqr' (default) → Tukey 1.5×IQR whiskers; points beyond // the fences render as outliers unless suppressed. - const props = ctx.chartProperties; const useMinMax = props?.whiskerMethod === 'minmax'; if (useMinMax) { spec.mark = setMarkProp(spec.mark, 'extent', 'min-max'); } // `showOutliers` defaults to true. With min-max whiskers there are no - // outliers anyway, so hiding them is implicit. - if (useMinMax || props?.showOutliers === false) { + // outliers anyway, so hiding them is implicit — and when every point is + // already drawn, the outlier marks would be duplicates. + if (useMinMax || props?.showOutliers === false || showPoints) { spec.mark = setMarkProp(spec.mark, 'outliers', false); } - const layout = ctx.layout; - const hasDiscreteX = layout.xNominalCount > 0; - const hasDiscreteAxis = hasDiscreteX || layout.yNominalCount > 0; + // Drawing the sample inverts the visual hierarchy. Normally the box is + // the figure and there is nothing behind it; once every observation is + // on the page the sample becomes the figure and the box demotes to + // scaffolding over it. So the box gives up its fill and keeps only an + // outline, and the colour encoding moves to the points — encoding the + // group twice, in fill and in point colour, would just be redundant ink. + // `filled: false` is the switch that redirects the colour encoding from + // the box's fill to its stroke. + if (showPoints) { + spec.mark = setMarkProp(spec.mark, 'box', { filled: false, strokeWidth: 1.5 }); + // The median rule is white by default so it reads against a filled + // box; over a hollow one it would disappear. It is the single + // most-read feature of a boxplot, so it gets the darkest ink at + // full strength (the boxplot theme dims marks to 0.7, which would + // let the point cloud show through it). + spec.mark = setMarkProp(spec.mark, 'median', { + color: SILHOUETTE_INK, strokeWidth: 2, opacity: 1, + }); + } // Grouped boxplots: a color field subdividing a categorical axis must // dodge the boxes side-by-side (xOffset/yOffset), not overlay them at the @@ -246,6 +280,13 @@ export const boxplotDef: ChartTemplateDef = { let subgroups = 1; let localSeparatorAxis: 'x' | 'y' | undefined; let localSeparatorValues: Record[] = []; + // How far a box is pushed off its band centre, as an expression in + // offset-scale domain units (the [-0.5, 0.5] domain spans one band, so + // one lane is `1 / subgroups` wide). '0' = undodged, sits dead centre. + // The point overlay reuses this so a point always lands on its own box. + let laneOffsetExpr = '0'; + // Transforms the lane expression depends on, replayed on the point layer. + let laneTransforms: Record[] = []; const colorField = ctx.channelSemantics?.color?.field; const axisField = hasDiscreteX ? ctx.channelSemantics?.x?.field @@ -267,19 +308,44 @@ export const boxplotDef: ChartTemplateDef = { // [-0.5, 0.5] range places each band's boxes centered, using // only maxPerBand lanes. Native axis labels stay centered. const maxPB = Math.max(1, plan.maxPerBand); + laneTransforms = [ + { window: [{ op: 'dense_rank', as: '__laneIdx' }], groupby: [axisField], sort: [{ field: colorField, order: 'ascending' }] }, + { joinaggregate: [{ op: 'distinct', field: colorField, as: '__localCount' }], groupby: [axisField] }, + ]; + laneOffsetExpr = `((datum.__laneIdx - 1) - (datum.__localCount - 1) / 2) / ${maxPB}`; spec.encoding[offsetChannel] = { field: '__off', type: 'quantitative', scale: { domain: [-0.5, 0.5] }, axis: null, }; spec.transform = [ ...(spec.transform ?? []), - { window: [{ op: 'dense_rank', as: '__laneIdx' }], groupby: [axisField], sort: [{ field: colorField, order: 'ascending' }] }, - { joinaggregate: [{ op: 'distinct', field: colorField, as: '__localCount' }], groupby: [axisField] }, - { calculate: `((datum.__laneIdx - 1) - (datum.__localCount - 1) / 2) / ${maxPB}`, as: '__off' }, + ...laneTransforms, + { calculate: laneOffsetExpr, as: '__off' }, ]; localSeparatorAxis = hasDiscreteX ? 'x' : 'y'; const categories = [...new Set((ctx.fullTable ?? ctx.table).map((row) => row[axisField]))]; localSeparatorValues = categories.slice(0, -1).map((category) => ({ [axisField]: category })); + } else if (showPoints) { + // Global lanes, with a point overlay. A *nominal* offset can + // carry a lane but not the extra jitter the points need, and + // one channel admits one scale — so resolve the lane index in + // the spec instead and let both layers share one quantitative + // offset. Lane order follows the declared colour sort, so the + // lanes still match the legend. + const laneOrder = (Array.isArray(colorEnc.sort) && colorEnc.sort.length > 0 + ? colorEnc.sort + : [...new Set((ctx.fullTable ?? ctx.table).map((row) => row[colorField]))].sort() + ).map((value) => String(value)); + subgroups = Math.max(1, laneOrder.length); + laneOffsetExpr = `(indexof(${JSON.stringify(laneOrder)}, toString(datum[${JSON.stringify(colorField)}])) - ${(subgroups - 1) / 2}) / ${subgroups}`; + spec.encoding[offsetChannel] = { + field: '__off', type: 'quantitative', + scale: { domain: [-0.5, 0.5] }, axis: null, + }; + spec.transform = [ + ...(spec.transform ?? []), + { calculate: laneOffsetExpr, as: '__off' }, + ]; } else { // Global: a fixed lane per distinct color across all bands. const offsetEnc: Record = { field: colorEnc.field, type: 'nominal' }; @@ -306,22 +372,73 @@ export const boxplotDef: ChartTemplateDef = { } } + let separatorLayer: Record | undefined; if (localSeparatorAxis && localSeparatorValues.length > 0) { - const boxLayer = { mark: spec.mark, encoding: spec.encoding, transform: spec.transform }; const axisEncoding = spec.encoding[localSeparatorAxis]; - spec.layer = [ - { - data: { values: localSeparatorValues }, - mark: { type: 'rule', stroke: '#c9ced6', strokeDash: [4, 4], strokeWidth: 1, opacity: 0.75 }, - encoding: { - [localSeparatorAxis]: { - field: axisField, - type: 'nominal', - sort: axisEncoding.sort, - bandPosition: 1, - }, + separatorLayer = { + data: { values: localSeparatorValues }, + mark: { type: 'rule', stroke: '#c9ced6', strokeDash: [4, 4], strokeWidth: 1, opacity: 0.75 }, + encoding: { + [localSeparatorAxis]: { + field: axisField, + type: 'nominal', + sort: axisEncoding.sort, + bandPosition: 1, + }, + }, + }; + } + + // The raw-observation overlay. Points ride on the same offset channel as + // the boxes: lane position (so a point sits on its own box) plus jitter + // (so coincident values do not stack into one dot). The offset scale's + // [-0.5, 0.5] domain maps onto the band, which keeps the cloud centred + // whatever the band width works out to — measuring jitter in pixels + // silently drifts off-centre when the layout changes the step. + let pointLayer: Record | undefined; + if (showPoints) { + const offsetChannel = hasDiscreteX ? 'xOffset' : 'yOffset'; + const lanePitch = ((hasDiscreteX ? layout.xStep : layout.yStep) * USABLE_BAND_FRACTION) / subgroups; + // `size` is point AREA in px²; keep the glyph well inside its lane. + const pointSize = Math.max(8, Math.min(30, Math.round(lanePitch * 0.6))); + const jitter = `(random() * 2 - 1) * ${(POINT_JITTER_FRACTION / subgroups).toFixed(5)}`; + pointLayer = { + transform: [ + ...laneTransforms, + { calculate: laneOffsetExpr === '0' ? jitter : `${laneOffsetExpr} + ${jitter}`, as: '__off' }, + ], + mark: { + // Points now carry the colour encoding (see the silhouette + // note above), so they are the one mark spending saturated + // ink. Slight transparency lets dense regions read as + // density; the hairline white halo keeps individual points + // countable where they overlap. + type: 'point', filled: true, size: pointSize, + opacity: 0.7, stroke: '#ffffff', strokeWidth: 0.5, + }, + encoding: { + ...(spec.encoding.x ? { x: JSON.parse(JSON.stringify(spec.encoding.x)) } : {}), + ...(spec.encoding.y ? { y: JSON.parse(JSON.stringify(spec.encoding.y)) } : {}), + ...(spec.encoding.color + ? { color: JSON.parse(JSON.stringify(spec.encoding.color)) } + : {}), + [offsetChannel]: { + field: '__off', type: 'quantitative', + scale: { domain: [-0.5, 0.5] }, axis: null, }, }, + }; + } + + if (separatorLayer || pointLayer) { + const boxLayer: Record = { mark: spec.mark, encoding: spec.encoding }; + if (spec.transform) boxLayer.transform = spec.transform; + // The hollow box goes on top of the cloud: it costs almost no ink to + // occlude, and the quartile edges and median have to stay crisp + // exactly where the points are densest. + spec.layer = [ + ...(separatorLayer ? [separatorLayer] : []), + ...(pointLayer ? [pointLayer] : []), boxLayer, ]; delete spec.mark; @@ -338,11 +455,24 @@ export const boxplotDef: ChartTemplateDef = { ], defaultValue: 'iqr', }, + { + key: 'showPoints', label: 'Points', type: 'binary', defaultValue: false, + // Jitter needs a band to scatter within, so this is only meaningful + // once one position axis is discrete. + check: (ctx) => ({ + applicable: isDiscreteType(ctx.channelSemantics?.x?.type) + || isDiscreteType(ctx.channelSemantics?.y?.type), + }), + }, { key: 'showOutliers', label: 'Outliers', type: 'binary', defaultValue: true, // Outliers exist only with Tukey whiskers; min–max whiskers absorb - // every point, so the toggle is irrelevant there. - check: (ctx) => ({ applicable: ctx.chartProperties?.whiskerMethod !== 'minmax' }), + // every point, so the toggle is irrelevant there. And once every + // observation is drawn, the outlier marks are just duplicates. + check: (ctx) => ({ + applicable: ctx.chartProperties?.whiskerMethod !== 'minmax' + && ctx.chartProperties?.showPoints !== true, + }), }, { key: 'dodge', label: 'Dodge', type: 'discrete', diff --git a/packages/flint-js/src/vegalite/templates/utils.ts b/packages/flint-js/src/vegalite/templates/utils.ts index 4f9a7095..db3ffeea 100644 --- a/packages/flint-js/src/vegalite/templates/utils.ts +++ b/packages/flint-js/src/vegalite/templates/utils.ts @@ -148,6 +148,60 @@ function maxNonOverlapSize( return Math.max(minSize, maxWidth); } +/** + * Make a stacked mark's segment order match its colour order. + * + * The assembler expresses "keep the order the data arrived in" as + * `color.sort: null`. That governs the legend, but NOT the stack: Vega-Lite + * derives the stack's sort from the colour *scale*, and a scale with no + * explicit domain falls back to sorting the field ascending — i.e. + * alphabetically. The result is a chart whose legend reads + * `A great deal, Some, Not much, None at all` while its bars stack + * `A great deal, None at all, Not much, Some`. For any ordered series + * (a Likert scale, an age band, a size class) that silently destroys the + * meaning of the stack. + * + * Pinning `scale.domain` to the same order makes Vega-Lite emit a + * `__sort_index` and sort the stack by it, so legend and stack agree. + * An explicit `sort` array does NOT achieve this — Vega-Lite applies it to + * the legend only — which is why the domain is what gets set here. + */ +export function alignStackOrderToColorOrder(spec: any, ctx: InstantiateContext): void { + const color = spec.encoding?.color; + // Only meaningful for a discrete colour series. + if (!color?.field || (color.type !== 'nominal' && color.type !== 'ordinal')) return; + // Called from stacked templates, where stacking is implicit unless it has + // been explicitly switched off (`stackMode: 'layered'` → `stack: null`). + const unstacked = (['x', 'y'] as const).some( + (axis) => spec.encoding?.[axis] && 'stack' in spec.encoding[axis] && + (spec.encoding[axis].stack === null || spec.encoding[axis].stack === false), + ); + if (unstacked) return; + // An explicit domain already pins the order; don't override the caller. + if (color.scale?.domain) return; + + // `sort: null` means data order; an array means that array. Anything else + // (a field-driven sort spec, "ascending"/"descending") is left alone. + let order: any[]; + if (color.sort === null) { + const seen = new Set(); + order = []; + for (const row of ctx.table ?? []) { + const v = row?.[color.field]; + if (v === undefined || v === null || seen.has(v)) continue; + seen.add(v); + order.push(v); + } + } else if (Array.isArray(color.sort)) { + order = color.sort; + } else { + return; + } + if (order.length < 2) return; + + color.scale = { ...(color.scale ?? {}), domain: order }; +} + /** * Adjust bar/rect marks for continuous-as-discrete axes. * v2 version: reads layout info from InstantiateContext. diff --git a/packages/flint-js/tests/boxplot-grouped-dodge.test.ts b/packages/flint-js/tests/boxplot-grouped-dodge.test.ts index 0025a93c..5cf1e607 100644 --- a/packages/flint-js/tests/boxplot-grouped-dodge.test.ts +++ b/packages/flint-js/tests/boxplot-grouped-dodge.test.ts @@ -283,7 +283,7 @@ describe('ECharts sparse grouped boxplot', () => { it('offsets global outliers onto their boxplot lane', () => { const option = assembleECharts(sparseInput('global')) as any; - const outliers = option.series.find((series: any) => series.name === 'G1 (outliers)'); + const outliers = option.series.find((series: any) => series.name === 'G1 (points)'); expect(outliers?.type).toBe('custom'); const api = { diff --git a/packages/flint-py/flint/vegalite/templates/area.py b/packages/flint-py/flint/vegalite/templates/area.py index 8b6809c5..2580b133 100644 --- a/packages/flint-py/flint/vegalite/templates/area.py +++ b/packages/flint-py/flint/vegalite/templates/area.py @@ -1,7 +1,7 @@ """Area Chart template.""" from __future__ import annotations -from .utils import default_build_encodings, set_mark_prop +from .utils import default_build_encodings, set_mark_prop, align_stack_order_to_color_order def _apply_interpolate(vg_spec, config): @@ -34,6 +34,7 @@ def _area_instantiate(spec, ctx): if enc and (enc.get("type") == "quantitative" or enc.get("aggregate")): spec["encoding"][axis]["stack"] = None if stack_mode == "layered" else stack_mode break + align_stack_order_to_color_order(spec, ctx) area_chart_def = { @@ -92,6 +93,7 @@ def _streamgraph_instantiate(spec, ctx): x_enc["stack"] = "center" x_enc["axis"] = None _apply_interpolate(spec, ctx.get("chartProperties")) + align_stack_order_to_color_order(spec, ctx) streamgraph_def = { diff --git a/packages/flint-py/flint/vegalite/templates/bar.py b/packages/flint-py/flint/vegalite/templates/bar.py index 1861d102..b1df159f 100644 --- a/packages/flint-py/flint/vegalite/templates/bar.py +++ b/packages/flint-py/flint/vegalite/templates/bar.py @@ -14,6 +14,7 @@ detect_banded_axis_from_semantics, detect_banded_axis_force_discrete, resolve_as_discrete, + align_stack_order_to_color_order, ) @@ -156,6 +157,7 @@ def _stacked_bar_instantiate(spec, ctx): if ae and (ae.get("type") == "quantitative" or ae.get("aggregate")): encoding[axis]["stack"] = None if config["stackMode"] == "layered" else config["stackMode"] break + align_stack_order_to_color_order(spec, ctx) adjust_bar_marks(spec, ctx) diff --git a/packages/flint-py/flint/vegalite/templates/candlestick.py b/packages/flint-py/flint/vegalite/templates/candlestick.py index 43b3a535..89102913 100644 --- a/packages/flint-py/flint/vegalite/templates/candlestick.py +++ b/packages/flint-py/flint/vegalite/templates/candlestick.py @@ -1,6 +1,9 @@ """Candlestick Chart template.""" from __future__ import annotations +from ...core import js_round +from .utils import adjust_bar_marks + def _candlestick_declare(cs, table, chart_properties): return {"axisFlags": {"x": {"banded": True}}} @@ -48,33 +51,59 @@ def _candlestick_instantiate(spec, ctx): spec["layer"][1]["encoding"]["y2"] = {"field": close["field"]} if open_ and open_.get("field") and close and close.get("field"): + # `<=`, not `<`: a session that closes exactly where it opened has not + # fallen, and colouring it as a decline is a false statement. spec["encoding"]["color"] = { "condition": { - "test": f"datum['{open_['field']}'] < datum['{close['field']}']", + "test": f"datum['{open_['field']}'] <= datum['{close['field']}']", "value": "#06982d", }, "value": "#ae1325", } - # Compute bar width from x-axis cardinality - table = ctx.get("table") or [] - canvas = ctx.get("canvasSize") or {} - plot_width = canvas.get("width") or 400 - x_field = (spec.get("encoding") or {}).get("x", {}).get("field") - if x_field and table: - seen = set() - for r in table: - seen.add(r.get(x_field)) - cardinality = max(1, len(seen)) - bar_size = max(2, min(20, round(plot_width * 0.6 / cardinality))) + # Body width. + # + # On a banded *continuous* x -- the usual case, dates -- the slot width is + # set by the smallest gap between observations, not by the row count: nine + # trading days spanning eleven calendar days occupy eleven slots, two of + # which are the weekend. Sizing on cardinality makes every body wider than + # its own slot and adjacent candles fuse into a single polygon. + # adjust_bar_marks() already performs that min-gap analysis for bar marks, + # so use it rather than keep a second, wrong copy of the arithmetic here. + # + # It returns the largest *non-overlapping* size, and bodies that merely + # touch still read as one shape when consecutive sessions move the same + # way. A candlestick needs a visible gutter, so take a fraction of it. + body_fill = 0.8 + layout = ctx.get("layout") or {} + if (layout.get("xContinuousAsDiscrete") or 0) > 0: + adjust_bar_marks(spec, ctx) + fitted = (spec["layer"][1].get("mark") or {}).get("size", 14) + bar_size = max(2, int(fitted * body_fill)) else: - bar_size = 14 + step = layout.get("xStep") or 20 + bar_size = max(2, js_round(step * body_fill)) layer1_mark = spec["layer"][1].get("mark") or {} if isinstance(layer1_mark, str): layer1_mark = {"type": layer1_mark} spec["layer"][1]["mark"] = {**layer1_mark, "size": bar_size} + # Doji sessions. + # + # When open == close the open->close bar has zero height and vanishes, so a + # flat session renders as a bare wick with no candle on it. Draw it as a + # horizontal tick at the shared price, which is the convention and is + # exactly what the bar degenerates to. + if open_ and open_.get("field") and close and close.get("field"): + spec["layer"].append({ + "transform": [ + {"filter": f"datum['{open_['field']}'] === datum['{close['field']}']"}, + ], + "mark": {"type": "tick", "size": bar_size, "thickness": 2}, + "encoding": {"y": {"field": close["field"]}}, + }) + candlestick_chart_def = { "chart": "Candlestick Chart", diff --git a/packages/flint-py/flint/vegalite/templates/utils.py b/packages/flint-py/flint/vegalite/templates/utils.py index 44b3c9b0..9c837cba 100644 --- a/packages/flint-py/flint/vegalite/templates/utils.py +++ b/packages/flint-py/flint/vegalite/templates/utils.py @@ -248,6 +248,64 @@ def max_non_overlap_size( return max(min_size, max_width) +def align_stack_order_to_color_order(spec: dict, ctx: dict) -> None: + """Make a stacked mark's segment order match its colour order. + + The assembler expresses "keep the order the data arrived in" as + ``color.sort: None``. That governs the legend, but NOT the stack: + Vega-Lite derives the stack's sort from the colour *scale*, and a scale + with no explicit domain falls back to sorting the field ascending — i.e. + alphabetically. The result is a chart whose legend reads + ``A great deal, Some, Not much, None at all`` while its bars stack + ``A great deal, None at all, Not much, Some``. For any ordered series + (a Likert scale, an age band, a size class) that silently destroys the + meaning of the stack. + + Pinning ``scale.domain`` to the same order makes Vega-Lite emit a + ``__sort_index`` and sort the stack by it, so legend and stack + agree. An explicit ``sort`` array does NOT achieve this — Vega-Lite + applies it to the legend only — which is why the domain is what gets set. + """ + encoding = spec.get("encoding") or {} + color = encoding.get("color") + # Only meaningful for a discrete colour series. + if not color or not color.get("field"): + return + if color.get("type") not in ("nominal", "ordinal"): + return + # Called from stacked templates, where stacking is implicit unless it has + # been explicitly switched off (``stackMode: 'layered'`` -> ``stack: None``). + for axis in ("x", "y"): + ae = encoding.get(axis) + if ae and "stack" in ae and ae["stack"] in (None, False): + return + # An explicit domain already pins the order; don't override the caller. + if (color.get("scale") or {}).get("domain"): + return + + # ``sort: None`` means data order; a list means that list. Anything else + # (a field-driven sort spec, "ascending"/"descending") is left alone. + sort = color.get("sort", "__missing__") + field = color["field"] + if sort is None: + order = [] + seen = set() + for row in ctx.get("table") or []: + value = (row or {}).get(field) + if value is None or value in seen: + continue + seen.add(value) + order.append(value) + elif isinstance(sort, list): + order = list(sort) + else: + return + if len(order) < 2: + return + + color["scale"] = {**(color.get("scale") or {}), "domain": order} + + def adjust_bar_marks(spec: dict, ctx: dict) -> None: """Adjust bar/rect marks for continuous-as-discrete axes.""" layout = ctx["layout"] diff --git a/site/src/components/VegaLiteView.tsx b/site/src/components/VegaLiteView.tsx index bdefd886..366aeadc 100644 --- a/site/src/components/VegaLiteView.tsx +++ b/site/src/components/VegaLiteView.tsx @@ -1,17 +1,17 @@ import { useEffect, useRef } from 'react'; import embed from 'vega-embed'; -export function VegaLiteView({ spec }: { spec: any }) { +export function VegaLiteView({ spec, renderer = 'canvas' }: { spec: any; renderer?: 'canvas' | 'svg' }) { const ref = useRef(null); useEffect(() => { if (!ref.current) return; let cancelled = false; - embed(ref.current, spec, { actions: false, renderer: 'canvas' }).catch((err) => { + embed(ref.current, spec, { actions: false, renderer }).catch((err) => { if (!cancelled) console.error('vega-embed failed', err); }); return () => { cancelled = true; }; - }, [spec]); + }, [spec, renderer]); return

; } diff --git a/site/src/main.tsx b/site/src/main.tsx index afa714e7..348a2dde 100644 --- a/site/src/main.tsx +++ b/site/src/main.tsx @@ -15,6 +15,7 @@ import { Illustrations } from './playground/Illustrations'; import { McpUi } from './playground/McpUi'; import { Labs } from './playground/Labs'; import { DemoWall } from './playground/DemoWall'; +import { ThemeLab } from './playground/ThemeLab'; import { FullTestCases } from './playground/FullTestCases'; import { LocaleProvider, useLocale } from './i18n/LocaleContext'; import type { Locale } from './i18n/locales'; @@ -52,6 +53,7 @@ function AppRoutes({ locale }: { locale: Locale }) { } /> } /> } /> + } /> } /> {/* Tutorials merged into Documentation as the "Quick start" group. */} diff --git a/site/src/playground/PlaygroundShell.tsx b/site/src/playground/PlaygroundShell.tsx index 755ee73f..076fe5a8 100644 --- a/site/src/playground/PlaygroundShell.tsx +++ b/site/src/playground/PlaygroundShell.tsx @@ -7,6 +7,7 @@ const pages = [ { to: 'mcp-ui', label: 'MCP UI test' }, { to: 'labs', label: 'Labs' }, { to: 'demo-wall', label: 'Demo wall' }, + { to: 'theme-labs', label: 'Theme lab' }, { to: 'full-test-cases', label: 'Full test cases' }, ]; diff --git a/site/src/playground/ThemeLab.tsx b/site/src/playground/ThemeLab.tsx new file mode 100644 index 00000000..6909e4b4 --- /dev/null +++ b/site/src/playground/ThemeLab.tsx @@ -0,0 +1,883 @@ +import { useEffect, useMemo, useState } from 'react'; +import { VegaLiteView } from '../components/VegaLiteView'; +import { ScaleToFit } from '../components/ScaleToFit'; +import { siteTheme } from '../shared/theme'; +import THEME_META from './theme-lab-assets/_themes.json'; +import FLINT_INDEX from './theme-lab-assets/_flint-index.json'; +import THEME_SPECS from './theme-lab-assets/_themespecs.json'; + +/** + * Theme lab — Flint's default Vega-Lite output next to a hand-authored + * bespoke redesign of the same chart, plus the diff between them. + * + * Nothing on this page is generated by a theming engine. Every "themed" spec + * in `theme-lab-assets/..json` was written by hand against the + * corresponding `.flint.json` baseline, so the third column is an honest + * inventory of what a design-theme language would actually have to control. + */ + +type ThemeId = keyof typeof THEME_META.themes; + +interface ThemeMeta { + label: string; + alias: string; + surface: string; + ink: string; + accent: string; + swatches: string[]; + intent: string; + signature: string[]; +} + +interface FlintIndexEntry { + id: string; + chartType: string; + title: string; + subtitle: string; + source: string; + rows: number; +} + +const THEMES = THEME_META.themes as unknown as Record; +const THEME_ORDER = THEME_META.order as ThemeId[]; + +// Eagerly pull in every spec in the assets folder. `.flint.json` is the +// baseline; `..json` is the hand-authored redesign. +const SPEC_MODULES = import.meta.glob('./theme-lab-assets/*.json', { eager: true }) as Record< + string, + { default: any } +>; + +interface LabRow { + id: string; + chartType: string; + title: string; + source: string; + rows: number; + theme: ThemeId; + flintSpec: any; + themedSpec: any; + design: string[]; +} + +function buildRows(): LabRow[] { + // One row per (id, theme). A case may carry several hand-authored themes — + // replicating one chart across every language is the only way to tell what + // the language decided from what the chart forced. + const flintById = new Map(); + const themedById = new Map(); + for (const [path, mod] of Object.entries(SPEC_MODULES)) { + const file = path.split('/').pop()!; + if (file.startsWith('_')) continue; + const match = /^(.*)\.([a-z-]+)\.json$/.exec(file); + if (!match) continue; + const [, id, kind] = match; + if (kind === 'flint') flintById.set(id, mod.default); + else themedById.set(id, [...(themedById.get(id) ?? []), mod.default]); + } + + const meta = new Map((FLINT_INDEX as FlintIndexEntry[]).map((e) => [e.id, e])); + const rows: LabRow[] = []; + for (const [id, themedList] of themedById) { + const flint = flintById.get(id); + if (!flint) continue; + const info = meta.get(id); + for (const themed of themedList) { + rows.push({ + id, + chartType: info?.chartType ?? '—', + title: info?.title ?? id, + source: info?.source ?? '', + rows: info?.rows ?? 0, + theme: themed.__theme__ as ThemeId, + flintSpec: flint, + themedSpec: themed, + design: (themed.__design__ as string[]) ?? [], + }); + } + } + // Group by case so a fully replicated set sits together on the wall. + return rows.sort( + (a, b) => a.id.localeCompare(b.id) || THEME_ORDER.indexOf(a.theme) - THEME_ORDER.indexOf(b.theme), + ); +} + +/** Vega-Lite ignores unknown top-level keys, but strip ours anyway. */ +function cleanSpec(spec: any): any { + const out: any = {}; + for (const [k, v] of Object.entries(spec)) { + if (!k.startsWith('__')) out[k] = v; + } + return out; +} + +function ThemeChip({ theme }: { theme: ThemeId }) { + const t = THEMES[theme]; + return ( + + + {t.label} + {t.alias} + + ); +} + +function SpecCell({ spec, dark, label }: { spec: any; dark: boolean; label: string }) { + const cleaned = useMemo(() => cleanSpec(spec), [spec]); + return ( +
+
+ {label} +
+
+ +
+
+ ); +} + +/** Small, uniform, shrink-to-fit chart tile used on the wall. */ +function Thumb({ spec, bg, height }: { spec: any; bg: string; height: number }) { + return ( +
+ + + +
+ ); +} + +/** + * One wall tile = one (chart, language) pair, baseline left, redesign right, + * small enough to scan a whole language in one screen. The argument for the + * pair lives in the popup, not here. + */ +function WallTile({ row, onOpen }: { row: LabRow; onOpen: () => void }) { + const t = THEMES[row.theme]; + const flint = useMemo(() => cleanSpec(row.flintSpec), [row.flintSpec]); + const themed = useMemo(() => cleanSpec(row.themedSpec), [row.themedSpec]); + return ( + + ); +} + +type CoverageStatus = 'full' | 'partial' | 'blocked'; +interface CoverageEntry { + status: CoverageStatus; + notes: string[]; +} + +const THEMESPEC_BY_THEME = THEME_SPECS.themes as unknown as Record; +const THEMESPEC_COVERAGE = THEME_SPECS.coverage as unknown as Record; + +/** How many charts each single ThemeSpec is on the hook for. */ +const ROWS_PER_THEME: Record = buildRows().reduce>((acc, r) => { + acc[r.theme as string] = (acc[r.theme as string] ?? 0) + 1; + return acc; +}, {}); + +const COVERAGE_INK: Record = { + full: '#2d8659', + partial: '#b7791f', + blocked: '#c0392b', +}; + +const COVERAGE_LABEL: Record = { + full: 'expressible', + partial: 'partly expressible', + blocked: 'not expressible', +}; + +/** Full-size pair, the design argument, and both raw specs. */ +function DetailModal({ row, onClose }: { row: LabRow; onClose: () => void }) { + const [showSpec, setShowSpec] = useState(false); + const [showThemeSpec, setShowThemeSpec] = useState(false); + const t = THEMES[row.theme]; + const themeSpec = THEMESPEC_BY_THEME[row.theme as string]; + const coverage: CoverageEntry = THEMESPEC_COVERAGE[row.id] ?? { status: 'full', notes: [] }; + const flint = useMemo(() => cleanSpec(row.flintSpec), [row.flintSpec]); + const themed = useMemo(() => cleanSpec(row.themedSpec), [row.themedSpec]); + + useEffect(() => { + const onKey = (e: KeyboardEvent) => { + if (e.key === 'Escape') onClose(); + }; + window.addEventListener('keydown', onKey); + return () => window.removeEventListener('keydown', onKey); + }, [onClose]); + + return ( +
+
e.stopPropagation()} + style={{ + width: 'min(100%, 1180px)', + background: siteTheme.surface, + border: `1px solid ${siteTheme.border}`, + borderRadius: 10, + padding: 20, + }} + > +
+ + + {row.id} + + + {row.chartType} · {row.rows} rows + + + + +
+ +
+ + +
+ +

+ What had to change ({row.design.length}) +

+
    + {row.design.map((d, i) => ( +
  1. + {d} +
  2. + ))} +
+ + {showThemeSpec && ( +
+

+ ThemeSpec · {t.label} +

+

+ One spec per design language, not one per chart. This is the whole of {t.label} — + the same object is what would have to produce all {' '} + {ROWS_PER_THEME[row.theme] ?? 0} of its charts in this lab. Written against{' '} + + themespec.v12 + + . +

+
+ + {COVERAGE_LABEL[coverage.status]} + + + for {row.id} + +
+ {coverage.notes.length > 0 && ( +
    + {coverage.notes.map((n, i) => ( +
  • + {n} +
  • + ))} +
+ )} +
{JSON.stringify(themeSpec, null, 2)}
+
+ )} + + {showSpec && ( +
+
{JSON.stringify(row.flintSpec, null, 2)}
+
{JSON.stringify(row.themedSpec, null, 2)}
+
+ )} +
+
+ ); +} + +const specPre: React.CSSProperties = { + margin: 0, + maxHeight: 340, + overflow: 'auto', + fontFamily: siteTheme.fontMono, + fontSize: 10.5, + lineHeight: 1.45, + background: '#fafafa', + border: `1px solid ${siteTheme.border}`, + borderRadius: 6, + padding: 10, + color: siteTheme.text, +}; + +export function ThemeLab() { + const rows = useMemo(buildRows, []); + const [filter, setFilter] = useState('all'); + const [openKey, setOpenKey] = useState(null); + const shown = filter === 'all' ? rows : rows.filter((r) => r.theme === filter); + const open = rows.find((r) => `${r.id}-${r.theme}` === openKey) ?? null; + const counts = useMemo(() => { + const m = new Map(); + rows.forEach((r) => m.set(r.theme, (m.get(r.theme) ?? 0) + 1)); + return m; + }, [rows]); + + return ( +
+
+
+

+ Theme lab{' '} + + ({rows.length} redesigns) + +

+

+ Every tile is a pair: on the left what Flint compiles today, on the right a Vega-Lite + spec written by hand against a named design language — no theming engine involved. + The wall is for scanning; click any tile for the full-size pair, the diff (the + concrete list of things a design-theme layer would have to be able to express) and + both raw specs. Tiles are grouped by chart, so a case themed in several languages + sits together. Specs live in site/src/playground/theme-lab-assets/, one + JSON per chart per theme, tagged with __theme__. +

+

+ Scope. Every redesign is reachable + from the same data and the same encoding. No annotation layers, no callouts, no + editorial headlines that state a conclusion, no invented reference lines, no source + credits. Those are worth studying later, but they need information the chart spec + does not carry. Nor may a redesign re-sort: which country leads, which age band sits + on top, which band rests on the baseline are statements about the data, decided + upstream in the Flint spec — where the baseline's order is wrong it is fixed there, + so both columns move together. What is in scope: geometry, scales, axis and grid + structure, palette, typography, legend placement, and re-encoding data that is + already present (for example printing a bar's value as a label). +

+

+ Text is held constant. Both columns + carry byte-identical title and subtitle strings, defined once in{' '} + _headlines.json. Flint's ChartAssemblyInput has no title + field at all, so the baseline is post-processed to stamp the headline on unstyled — + otherwise column 2 would win simply by having words on the page. The difference you + are looking at is typography, anchoring, spacing and colour, never wording.{' '} + scripts/check-theme-lab.mjs fails the build if the two ever drift apart. +

+
+ +
+ + {THEME_ORDER.map((id) => ( + + ))} +
+ +
+ + The {THEME_ORDER.length} design languages — palette, intent, signature + +
+ {THEME_ORDER.filter((id) => filter === 'all' || filter === id).map((id) => { + const t = THEMES[id]; + return ( +
+
+ {t.label} + {t.alias} +
+
+ {t.swatches.map((c) => ( + + ))} + +
+

+ {t.intent} +

+
    + {t.signature.map((s, i) => ( +
  • {s}
  • + ))} +
+
+ ); + })} +
+
+ +
+ {shown.map((row) => ( + setOpenKey(`${row.id}-${row.theme}`)} + /> + ))} +
+ + {open && setOpenKey(null)} />} + + {shown.length === 0 && ( +

+ No hand-authored specs for this theme yet. +

+ )} + + +
+
+ ); +} + +/** + * What the table covers, what it does not, and which additional cases would + * actually buy new information about the design space. + */ +function CoverageNotes({ rows }: { rows: LabRow[] }) { + const index = FLINT_INDEX as FlintIndexEntry[]; + const done = new Set(rows.map((r) => r.id)); + const coveredTypes = new Set(rows.map((r) => r.chartType)); + const untouched = index.filter((e) => !done.has(e.id)); + const untouchedNew = untouched.filter((e) => !coveredTypes.has(e.chartType)); + const themesPerCase = new Map(); + rows.forEach((r) => themesPerCase.set(r.id, (themesPerCase.get(r.id) ?? 0) + 1)); + const full = [...themesPerCase.entries()].filter(([, n]) => n >= THEME_ORDER.length); + const once = [...themesPerCase.values()].filter((n) => n === 1).length; + + return ( +
+

+ Coverage, and what is still missing +

+

+ {rows.length} redesigns across {coveredTypes.size} chart types and {THEME_ORDER.length}{' '} + design languages, drawn from {index.length} Flint baselines. A case earns its place by + forcing a decision no theme here has had to make yet — more charts is not the same as more + evidence. +

+ +

+ The largest gap is not a missing chart.{' '} + {full.length === 0 + ? `No case carries all ${THEME_ORDER.length} languages, and ${once} of ${themesPerCase.size} are themed exactly once.` + : `${full.length} of ${themesPerCase.size} cases carry all ${THEME_ORDER.length} languages (${full + .map(([id]) => id) + .join(', ')}); ${once} are themed exactly once.`}{' '} + Themed once, a row cannot separate what the language decided from what the chart + demanded. +
+
+ Three replicated sets sort the decisions into two kinds.{' '} + Print the values or keep the axis splits the six on the bar — NYT, McKinsey, + Datawrapper and Power BI print; the Economist and Nature trust the axis — and then stops + meaning anything: on the pie there is no axis to trust, and all six print.{' '} + Legend or direct labels holds: NYT, the Economist and McKinsey label directly on + both the line and the pie; Nature, Datawrapper and Power BI keep a key on both. The first + is the chart talking, the second is the house — and a single-case table would have called + both style. +
+
+ So what a theme layer has to encode is an attitude to scaffolding, not a rule about + marks. Nature keeps the most, and is the only language that answers a problem by adding + an encoding rather than removing one. McKinsey keeps the least. +

+ +
+ + e.chartType).join(', ')}). The other ${untouched.length - untouchedNew.length} are repeats of covered types and buy nothing — a second scatter tests the same decisions as the first.`, + 'Calendar heatmaps and cycle plots — seasonality is a layout question no case here asks.', + 'Point-symbol maps. The choropleth settles class breaks and hue ramps; a size legend floating over a basemap is untested.', + 'Radial forms — donut, rose, radar sit unpaired. Angle and area are the hardest channels for a language to legislate, and the pie is the only one of the four any of these six has had to argue.', + 'Funnel and gauge — Plotly-only in Flint, so the baseline column cannot be produced at all.', + ]} + /> + +
+ +
+ + Baselines with no bespoke counterpart yet ({untouched.length}) + +
+ {untouched.map((e) => ( + + {e.id}{' '} + · {e.chartType} + + ))} +
+
+
+ ); +} + +function GapCard({ title, tone, items }: { title: string; tone: 'done' | 'gap'; items: string[] }) { + return ( +
+ {title} +
    + {items.map((s, i) => ( +
  • + {s} +
  • + ))} +
+
+ ); +} + +function filterBtn(active: boolean): React.CSSProperties { + return { + fontSize: 12, + padding: '4px 12px', + borderRadius: 999, + border: `1px solid ${active ? siteTheme.accent : siteTheme.border}`, + background: active ? siteTheme.accentBg : 'transparent', + color: active ? siteTheme.accent : siteTheme.text, + cursor: 'pointer', + }; +} diff --git a/site/src/playground/new-case-preview-data.ts b/site/src/playground/new-case-preview-data.ts index a4a4b299..612d298e 100644 --- a/site/src/playground/new-case-preview-data.ts +++ b/site/src/playground/new-case-preview-data.ts @@ -106,6 +106,22 @@ const LIFE_EXP: Record = { Nigeria: [46.6, 52.7], Japan: [81.1, 84.5], Russia: [65.5, 70.1], Brazil: [70.1, 72.8], }; +// --------------------------------------------------------------------------- +// 8. Fifty states, one rate — drives both the choropleth and the fifty-colour bar +// --------------------------------------------------------------------------- +const STATE_UNEMPLOYMENT: [string, number][] = [ + ['Alabama', 2.3], ['Alaska', 4.3], ['Arizona', 3.9], ['Arkansas', 3.3], ['California', 4.8], + ['Colorado', 3.2], ['Connecticut', 3.9], ['Delaware', 4.2], ['Florida', 2.9], ['Georgia', 3.2], + ['Hawaii', 3.0], ['Idaho', 3.2], ['Illinois', 4.5], ['Indiana', 3.3], ['Iowa', 2.9], + ['Kansas', 2.8], ['Kentucky', 4.2], ['Louisiana', 3.5], ['Maine', 2.9], ['Maryland', 2.1], + ['Massachusetts', 3.3], ['Michigan', 3.9], ['Minnesota', 2.8], ['Mississippi', 3.2], ['Missouri', 3.0], + ['Montana', 2.9], ['Nebraska', 2.4], ['Nevada', 5.3], ['New Hampshire', 2.4], ['New Jersey', 4.1], + ['New Mexico', 3.8], ['New York', 4.1], ['North Carolina', 3.4], ['North Dakota', 1.9], ['Ohio', 3.6], + ['Oklahoma', 3.0], ['Oregon', 3.7], ['Pennsylvania', 3.4], ['Rhode Island', 2.9], ['South Carolina', 3.0], + ['South Dakota', 1.9], ['Tennessee', 3.2], ['Texas', 4.0], ['Utah', 2.5], ['Vermont', 2.1], + ['Virginia', 2.8], ['Washington', 4.1], ['West Virginia', 4.0], ['Wisconsin', 2.9], ['Wyoming', 3.1], +]; + function buildCases(): PreviewCase[] { const cases: PreviewCase[] = []; @@ -153,7 +169,7 @@ function buildCases(): PreviewCase[] { source: 'UN World Population Prospects 2022 / World Bank (2023)', license: 'CC-BY 4.0 (attribute UN/World Bank)', semantic_types: { Country: 'Country', Population: 'Quantity' }, - encodings: { x: 'Country', y: 'Population' }, + encodings: { y: 'Country', x: 'Population' }, data: [ ['India', 1428.6], ['China', 1425.7], ['United States', 339.9], ['Indonesia', 277.5], ['Pakistan', 240.5], ['Nigeria', 223.8], ['Brazil', 216.4], ['Bangladesh', 173.0], @@ -459,8 +475,10 @@ function buildCases(): PreviewCase[] { semantic_types: { Age: 'Category', Sex: 'Category', Population: 'Quantity' }, encodings: { x: 'Population', y: 'Age', color: 'Sex' }, data: (() => { - const ages = ['0–14', '15–29', '30–44', '45–59', '60–74', '75+']; - const m = [31, 34, 33, 30, 26, 10], f = [30, 32, 33, 31, 28, 15]; + // Oldest first: an ordinal axis renders its domain top-down, and a population + // pyramid is read with age increasing upward. + const ages = ['75+', '60–74', '45–59', '30–44', '15–29', '0–14']; + const m = [10, 26, 30, 33, 34, 31], f = [15, 28, 31, 33, 32, 30]; const rows: Record[] = []; ages.forEach((Age, i) => { rows.push({ Age, Sex: 'Male', Population: m[i] }); rows.push({ Age, Sex: 'Female', Population: f[i] }); }); return rows; @@ -745,9 +763,9 @@ function buildCases(): PreviewCase[] { blurb: 'Bars cross zero — cool early decades below, rapid warming above.', source: 'NASA GISTEMP (approx. decadal means)', license: 'US-gov (PD)', - semantic_types: { Decade: 'Category', 'Anomaly (°C)': 'Quantity' }, - encodings: { x: 'Decade', y: 'Anomaly (°C)' }, - data: [['1880s', -0.17], ['1900s', -0.16], ['1920s', -0.27], ['1940s', 0.12], ['1960s', -0.03], ['1980s', 0.26], ['2000s', 0.40], ['2010s', 0.72], ['2020s', 1.02]].map(([Decade, v]) => ({ Decade, 'Anomaly (°C)': v })), + semantic_types: { Decade: 'Category', 'Anomaly (°C)': 'Quantity', Direction: 'Category' }, + encodings: { x: 'Decade', y: 'Anomaly (°C)', color: 'Direction' }, + data: [['1880s', -0.17], ['1900s', -0.16], ['1920s', -0.27], ['1940s', 0.12], ['1960s', -0.03], ['1980s', 0.26], ['2000s', 0.40], ['2010s', 0.72], ['2020s', 1.02]].map(([Decade, v]) => ({ Decade, 'Anomaly (°C)': v, Direction: (v as number) < 0 ? 'Below average' : 'Above average' })), }); // US unemployment 2000–2023 — the recession spikes. @@ -875,10 +893,281 @@ function buildCases(): PreviewCase[] { source: 'The Economist Big Mac index (approx. 2023)', license: 'Illustrative (The Economist)', semantic_types: { Country: 'Country', 'Price (USD)': 'Quantity' }, - encodings: { x: 'Country', y: 'Price (USD)' }, + encodings: { x: 'Price (USD)', y: 'Country' }, data: [['Switzerland', 8.1], ['Norway', 6.9], ['United States', 5.7], ['Euro area', 5.5], ['UK', 4.9], ['Brazil', 4.5], ['Mexico', 3.9], ['China', 3.5], ['Japan', 3.2], ['Egypt', 2.7], ['South Africa', 2.6], ['India', 2.5]].map(([Country, p]) => ({ Country, 'Price (USD)': p })), }); + // ── Coverage cases ────────────────────────────────────────────────────── + // The set above is broad on chart *type* but narrow on chart *shape*: almost + // every case is a single un-faceted panel with one series. These add the + // configurations that dominate real reporting — small multiples, 100% + // stacks, diverging Likert bars, step lines, dashed projections and a + // choropleth — so the theme lab has something to say about layout, not just + // colour. + + // Small multiples: the same line repeated per country. + cases.push({ + id: 'oecd-unemployment-facet', + chartType: 'Line Chart', + title: 'Unemployment rate by country, 2000–2022 (%)', + blurb: 'Small multiples — four labour markets that behaved nothing alike.', + source: 'OECD Labour Force Statistics (harmonised unemployment rate)', + license: 'OECD terms (facts re-keyed)', + semantic_types: { Year: 'Year', Country: 'Country', 'Unemployment (%)': 'Quantity' }, + encodings: { x: 'Year', y: 'Unemployment (%)', column: 'Country' }, + data: ([ + ['United States', [4.0, 5.1, 9.6, 5.3, 8.1, 3.6]], + ['Germany', [7.9, 11.2, 7.0, 4.6, 3.7, 3.1]], + ['Japan', [4.7, 4.4, 5.1, 3.4, 2.8, 2.6]], + ['Spain', [13.9, 9.2, 19.9, 22.1, 15.5, 12.9]], + ] as [string, number[]][]).flatMap(([Country, series]) => + [2000, 2005, 2010, 2015, 2020, 2022].map((Year, i) => ({ Year, Country, 'Unemployment (%)': series[i] })), + ), + }); + + // 100% stacked bar — composition, not magnitude. + cases.push({ + id: 'spending-quintile', + chartType: 'Stacked Bar Chart', + title: 'Where each income group\'s money goes (share of spending)', + blurb: 'A 100% stack: housing eats twice the share at the bottom that it does at the top.', + source: 'US Bureau of Labor Statistics, Consumer Expenditure Survey (approx. 2022)', + license: 'US-gov (PD)', + semantic_types: { Quintile: 'Category', Category: 'Category', 'Spending ($)': 'Quantity' }, + encodings: { x: 'Quintile', y: 'Spending ($)', color: 'Category' }, + chartProperties: { stackMode: 'normalize' }, + data: ([ + ['Lowest fifth', [12800, 4900, 5100, 2900, 6400]], + ['Second fifth', [16100, 7600, 6300, 4000, 10700]], + ['Middle fifth', [19400, 10600, 7600, 4700, 16400]], + ['Fourth fifth', [24300, 13700, 9100, 6100, 22800]], + ['Highest fifth', [36600, 19500, 13400, 8500, 43800]], + ] as [string, number[]][]).flatMap(([Quintile, vals]) => + ['Housing', 'Transportation', 'Food', 'Healthcare', 'Everything else'].map((Category, i) => ({ + Quintile, Category, 'Spending ($)': vals[i], + })), + ), + }); + + // Diverging stacked bar — the standard survey/Likert layout. + cases.push({ + id: 'trust-likert', + chartType: 'Stacked Bar Chart', + title: 'Confidence in US institutions (% of adults)', + blurb: 'A Likert bar centred on the neutral split — agreement left, doubt right.', + source: 'Pew Research Center / Gallup confidence-in-institutions series (approx. 2023)', + license: 'Illustrative (figures re-keyed)', + semantic_types: { Institution: 'Category', Response: 'Category', 'Share (%)': 'Quantity' }, + encodings: { x: 'Share (%)', y: 'Institution', color: 'Response' }, + chartProperties: { stackMode: 'center' }, + data: ([ + ['Scientists', [39, 45, 12, 4]], + ['The military', [32, 43, 18, 7]], + ['The police', [26, 44, 21, 9]], + ['The press', [11, 32, 34, 23]], + ['Congress', [8, 30, 38, 24]], + ] as [string, number[]][]).flatMap(([Institution, vals]) => + ['A great deal', 'Some', 'Not much', 'None at all'].map((Response, i) => ({ + Institution, Response, 'Share (%)': vals[i], + })), + ), + }); + + // Step line — a policy rate only ever moves in jumps. + cases.push({ + id: 'fed-funds-step', + chartType: 'Line Chart', + title: 'Federal funds target rate, 2015–2024 (%, upper bound)', + blurb: 'A rate that only moves at meetings — interpolation would lie about it.', + source: 'US Federal Reserve, FOMC target range (year-end upper bound)', + license: 'US-gov (PD)', + semantic_types: { Year: 'Year', 'Target rate (%)': 'Quantity' }, + encodings: { x: 'Year', y: 'Target rate (%)' }, + chartProperties: { interpolate: 'step' }, + data: [[2015, 0.5], [2016, 0.75], [2017, 1.5], [2018, 2.5], [2019, 1.75], [2020, 0.25], [2021, 0.25], [2022, 4.5], [2023, 5.5], [2024, 4.75]] + .map(([Year, v]) => ({ Year, 'Target rate (%)': v })), + }); + + // Dashed projection — observed and forecast in one line. + cases.push({ + id: 'renewables-projection', + chartType: 'Line Chart', + title: 'Global renewable capacity, observed and projected (GW)', + blurb: 'One measure, two epistemic states — the dash carries the difference.', + source: 'IEA Renewables market report (approx. figures)', + license: 'Illustrative (figures re-keyed)', + semantic_types: { Year: 'Year', Series: 'Category', 'Capacity (GW)': 'Quantity' }, + encodings: { x: 'Year', y: 'Capacity (GW)', strokeDash: 'Series' }, + data: [ + ...[[2015, 785], [2017, 1080], [2019, 1440], [2021, 1900], [2023, 2560]].map(([Year, v]) => ({ Year, Series: 'Observed', 'Capacity (GW)': v })), + ...[[2023, 2560], [2025, 3400], [2027, 4300], [2030, 5800]].map(([Year, v]) => ({ Year, Series: 'Projected', 'Capacity (GW)': v })), + ], + }); + + // Horizontal grouped bar — the survey-topline shape. + cases.push({ + id: 'earnings-education', + chartType: 'Grouped Bar Chart', + title: 'Median weekly earnings by education and sex, 2023 ($)', + blurb: 'Two gaps at once: the education ladder and the gap inside every rung.', + source: 'US Bureau of Labor Statistics, usual weekly earnings (2023 annual)', + license: 'US-gov (PD)', + semantic_types: { Education: 'Category', Sex: 'Category', 'Weekly earnings ($)': 'Quantity' }, + encodings: { x: 'Weekly earnings ($)', y: 'Education', group: 'Sex' }, + data: ([ + // Highest attainment first: an ordinal axis renders its domain top-down, so this + // puts the top of the education ladder at the top of the chart. + ['Advanced degree', [2160, 1600]], + ['Bachelor\'s degree', [1700, 1290]], + ['Some college', [1120, 890]], + ['High school', [1000, 790]], + ['Less than high school', [780, 620]], + ] as [string, number[]][]).flatMap(([Education, vals]) => + ['Men', 'Women'].map((Sex, i) => ({ Education, Sex, 'Weekly earnings ($)': vals[i] })), + ), + }); + + // 100% stacked area — share of a whole, over time. + cases.push({ + id: 'electricity-mix-area', + chartType: 'Area Chart', + title: 'World electricity generation by source, 1990–2020 (share)', + blurb: 'Coal holds its share for thirty years while wind and solar appear from nothing.', + source: 'IEA / Ember global electricity review (approx. shares)', + license: 'Illustrative (figures re-keyed)', + semantic_types: { Year: 'Year', Source: 'Category', 'Generation (TWh)': 'Quantity' }, + encodings: { x: 'Year', y: 'Generation (TWh)', color: 'Source' }, + chartProperties: { stackMode: 'normalize' }, + data: ([ + [1990, [4430, 1780, 2160, 2000, 10, 1580]], + [2000, [5990, 2760, 2620, 2590, 80, 1310]], + [2010, [8670, 4760, 3440, 2760, 380, 1520]], + [2020, [9420, 6270, 4360, 2700, 2720, 1610]], + ] as [number, number[]][]).flatMap(([Year, vals]) => + ['Coal', 'Gas', 'Hydro', 'Nuclear', 'Wind & solar', 'Other'].map((Source, i) => ({ + Year, Source, 'Generation (TWh)': vals[i], + })), + ), + }); + + // Choropleth — the one VL template the corpus never exercised. + cases.push({ + id: 'state-unemployment', + chartType: 'Choropleth', + title: 'Unemployment rate by state, 2023 (%)', + blurb: 'The only registered Vega-Lite template the gallery never used.', + source: 'US Bureau of Labor Statistics, Local Area Unemployment Statistics (2023 annual)', + license: 'US-gov (PD)', + semantic_types: { State: 'State', 'Unemployment (%)': 'Quantity' }, + encodings: { id: 'State', color: 'Unemployment (%)' }, + data: STATE_UNEMPLOYMENT.map(([State, v]) => ({ State, 'Unemployment (%)': v })), + }); + + // Long-label horizontal bars — the label is longer than the bar it names. + cases.push({ + id: 'causes-death', + chartType: 'Bar Chart', + title: 'Leading causes of death, United States, 2022', + blurb: 'Every category name is a clinical phrase, and several are longer than the bar they label.', + source: 'CDC / National Center for Health Statistics, leading causes of death (approx. 2022)', + license: 'US-gov (PD); figures re-keyed', + semantic_types: { Cause: 'Category', 'Deaths (thousands)': 'Quantity' }, + encodings: { y: 'Cause', x: 'Deaths (thousands)' }, + data: [ + ['Diseases of heart', 703], + ['Malignant neoplasms', 608], + ['Unintentional injuries', 227], + ['Cerebrovascular diseases', 165], + ['Chronic lower respiratory diseases', 148], + ['Alzheimer disease', 120], + ['Diabetes mellitus', 102], + ['Nephritis, nephrotic syndrome and nephrosis', 58], + ['Chronic liver disease and cirrhosis', 55], + ['Intentional self-harm (suicide)', 49], + ].map(([Cause, v]) => ({ Cause, 'Deaths (thousands)': v })), + }); + + // Fifty categories on one axis — past the point where a category axis can label itself. + cases.push({ + id: 'state-jobless', + chartType: 'Bar Chart', + title: 'Unemployment rate by state, 2023 (%)', + blurb: 'Fifty categories on a single axis, in source order, each needing a readable name.', + source: 'US Bureau of Labor Statistics, Local Area Unemployment Statistics (2023 annual)', + license: 'US-gov (PD)', + semantic_types: { State: 'State', 'Unemployment (%)': 'Quantity' }, + encodings: { x: 'State', y: 'Unemployment (%)' }, + data: STATE_UNEMPLOYMENT.map(([State, v]) => ({ State, 'Unemployment (%)': v })), + }); + + // Dense small multiples — sixteen panels, past the point where each keeps an axis. + cases.push({ + id: 'oecd-facet-16', + chartType: 'Line Chart', + title: 'Unemployment rate, sixteen OECD economies, 2000–2023 (%)', + blurb: 'Sixteen panels: too many for every panel to carry its own axis, too few to give up on labels.', + source: 'OECD Labour Force Statistics (harmonised unemployment rate, approx.)', + license: 'Illustrative (figures re-keyed)', + semantic_types: { Year: 'Year', Country: 'Country', 'Unemployment (%)': 'Quantity' }, + encodings: { x: 'Year', y: 'Unemployment (%)', column: 'Country' }, + data: ([ + ['Australia', [6.3, 4.4, 5.2, 6.1, 3.7]], + ['Austria', [4.7, 4.9, 4.8, 5.7, 5.1]], + ['Belgium', [6.9, 7.5, 8.3, 8.5, 5.5]], + ['Canada', [6.8, 6.0, 8.0, 6.9, 5.4]], + ['Denmark', [4.3, 3.8, 7.5, 6.2, 5.1]], + ['Finland', [9.8, 6.9, 8.4, 9.4, 7.2]], + ['France', [9.0, 8.0, 9.3, 10.4, 7.3]], + ['Germany', [7.9, 8.7, 7.0, 4.6, 3.1]], + ['Ireland', [4.2, 4.7, 13.9, 9.9, 4.3]], + ['Italy', [10.1, 6.1, 8.4, 11.9, 7.7]], + ['Japan', [4.7, 3.8, 5.1, 3.4, 2.6]], + ['Netherlands', [3.1, 3.6, 5.0, 6.9, 3.6]], + ['Norway', [3.2, 2.5, 3.6, 4.4, 3.6]], + ['Spain', [13.9, 8.2, 19.9, 22.1, 12.2]], + ['Sweden', [5.6, 6.1, 8.6, 7.4, 7.7]], + ['United States', [4.0, 4.6, 9.6, 5.3, 3.6]], + ] as [string, number[]][]).flatMap(([Country, vals]) => + [2000, 2007, 2010, 2015, 2023].map((Year, i) => ({ + Year, Country, 'Unemployment (%)': vals[i], + })), + ), + }); + + // Uncertainty drawn as geometry — a 95% interval that narrows as the record improves. + cases.push({ + id: 'temp-uncertainty', + chartType: 'Range Area Chart', + title: 'Global mean temperature anomaly with its 95% interval, 1850–2023', + blurb: 'The band is nine times wider in 1850 than in 2023 — the measurement record improving, drawn as geometry.', + source: 'Met Office Hadley Centre / UEA CRU, HadCRUT5 global annual anomaly vs 1961–1990 (approx.)', + license: 'Illustrative (figures re-keyed)', + semantic_types: { + Year: 'Year', + 'Lower (°C)': 'Quantity', + 'Upper (°C)': 'Quantity', + 'Anomaly (°C)': 'Quantity', + }, + encodings: { x: 'Year', y: 'Lower (°C)', y2: 'Upper (°C)' }, + data: ([ + [1850, -0.42, -0.62, -0.22], + [1870, -0.36, -0.53, -0.19], + [1890, -0.42, -0.56, -0.28], + [1910, -0.44, -0.55, -0.33], + [1930, -0.16, -0.25, -0.07], + [1950, -0.17, -0.24, -0.10], + [1970, -0.08, -0.14, -0.02], + [1990, 0.25, 0.20, 0.30], + [2010, 0.56, 0.52, 0.60], + [2023, 1.11, 1.07, 1.15], + ] as [number, number, number, number][]).map(([Year, est, lo, hi]) => ({ + Year, + 'Anomaly (°C)': est, + 'Lower (°C)': lo, + 'Upper (°C)': hi, + })), + }); + return cases; } diff --git a/site/src/playground/theme-lab-assets/_flint-index.json b/site/src/playground/theme-lab-assets/_flint-index.json new file mode 100644 index 00000000..206dd5a8 --- /dev/null +++ b/site/src/playground/theme-lab-assets/_flint-index.json @@ -0,0 +1,474 @@ +[ + { + "id": "driving", + "chartType": "Connected Scatter Plot", + "title": "Driving shifts into reverse", + "subtitle": "Miles driven per person against the price of a gallon of gas, United States, 1956–2010", + "source": "NYT / Hannah Fairfield (2010), from US FHWA vehicle-miles + EIA/BLS gas prices; via vega-datasets driving.json", + "rows": 55 + }, + { + "id": "penguins", + "chartType": "Scatter Plot", + "title": "Flipper length versus body mass in three penguin species", + "subtitle": "Palmer Archipelago, Antarctica; n = 33", + "source": "Horst, Hill & Gorman (2020), Palmer Station LTER — 33-row sample", + "rows": 33 + }, + { + "id": "keeling", + "chartType": "Line Chart", + "title": "Keeling Curve", + "subtitle": "Atmospheric CO₂ at Mauna Loa, annual mean, parts per million", + "source": "Scripps CO₂ Program / NOAA GML, Mauna Loa Observatory", + "rows": 14 + }, + { + "id": "population", + "chartType": "Bar Chart", + "title": "Most populous countries, 2023", + "subtitle": "Population in millions", + "source": "UN World Population Prospects 2022 / World Bank (2023)", + "rows": 10 + }, + { + "id": "population-region", + "chartType": "Area Chart", + "title": "World population by region", + "subtitle": "1950–2020, millions of people", + "source": "UN World Population Prospects 2022", + "rows": 25 + }, + { + "id": "faithful", + "chartType": "Scatter Plot", + "title": "Old Faithful eruptions — waiting time vs duration", + "subtitle": "", + "source": "Härdle (1991) / R \"faithful\" dataset — 36-row sample", + "rows": 36 + }, + { + "id": "life-expectancy", + "chartType": "Slope Chart", + "title": "Two decades of longer lives", + "subtitle": "Life expectancy at birth, years, 2000 and 2021", + "source": "World Bank / Our World in Data", + "rows": 14 + }, + { + "id": "population-waterfall", + "chartType": "Waterfall Chart", + "title": "Asia added more people than the world held in 1950", + "subtitle": "Contribution to world population growth by region, 1950–2020, millions", + "source": "UN World Population Prospects 2022 (regional deltas)", + "rows": 6 + }, + { + "id": "nutrition-radar", + "chartType": "Radar Chart", + "title": "Nutrition profile per 100 g — Almonds vs Oats vs Greek yogurt", + "subtitle": "", + "source": "USDA FoodData Central (representative per-100g values)", + "rows": 15 + }, + { + "id": "auto-mpg", + "chartType": "Regression", + "title": "Power against fuel economy", + "subtitle": "Ordinary least squares fit; each point is one car model", + "source": "UCI / StatLib Auto MPG dataset (sample)", + "rows": 22 + }, + { + "id": "faithful-hist", + "chartType": "Histogram", + "title": "Old Faithful eruption durations", + "subtitle": "Two clusters, not one: short eruptions near 2 min and long ones near 4 min", + "source": "R \"faithful\" dataset (sample)", + "rows": 36 + }, + { + "id": "faithful-density", + "chartType": "Density Plot", + "title": "Old Faithful — eruption duration density", + "subtitle": "", + "source": "R \"faithful\" dataset (sample)", + "rows": 36 + }, + { + "id": "penguins-box", + "chartType": "Boxplot", + "title": "Body mass of three Pygoscelis species", + "subtitle": "Boxes show median and interquartile range; whiskers span the full sample; points are individual birds", + "source": "Palmer Station LTER (sample)", + "rows": 33 + }, + { + "id": "penguins-violin", + "chartType": "Violin Plot", + "title": "Body mass by penguin species", + "subtitle": "Kernel density with all observations overlaid; horizontal rule marks the median", + "source": "Palmer Station LTER (sample)", + "rows": 33 + }, + { + "id": "iris-strip", + "chartType": "Strip Plot", + "title": "Iris petal length by species", + "subtitle": "", + "source": "Fisher's Iris (1936), sample", + "rows": 30 + }, + { + "id": "exam-ecdf", + "chartType": "ECDF Plot", + "title": "Empirical distribution of exam scores", + "subtitle": "n = 30; circles mark individual scores", + "source": "Illustrative class scores", + "rows": 30 + }, + { + "id": "medals-grouped", + "chartType": "Grouped Bar Chart", + "title": "Paris 2024 Olympic medals — top nations", + "subtitle": "", + "source": "IOC, Paris 2024 medal table", + "rows": 15 + }, + { + "id": "electricity-stacked", + "chartType": "Stacked Bar Chart", + "title": "Electricity generation mix by country, 2023 (%)", + "subtitle": "", + "source": "Our World in Data / Ember (approx.)", + "rows": 15 + }, + { + "id": "co2-lollipop", + "chartType": "Lollipop Chart", + "title": "Emissions per person run from 37 tonnes to 2", + "subtitle": "Carbon dioxide emissions per capita, 2022, tonnes", + "source": "Our World in Data (Global Carbon Project)", + "rows": 12 + }, + { + "id": "us-pyramid", + "chartType": "Pyramid Chart", + "title": "A pyramid that is no longer a pyramid", + "subtitle": "United States population by age and sex, 2020, millions", + "source": "US Census 2020 (approx.)", + "rows": 12 + }, + { + "id": "gdp-bartable", + "chartType": "Bar Table", + "title": "America and China lap the field", + "subtitle": "Gross domestic product, 2023, trillion US dollars", + "source": "IMF / World Bank 2023", + "rows": 8 + }, + { + "id": "lifeexp-dumbbell", + "chartType": "Ranged Dot Plot", + "title": "Women outlive men everywhere, but not by the same margin", + "subtitle": "Life expectancy at birth by sex, 2021, years", + "source": "World Bank 2021", + "rows": 12 + }, + { + "id": "seattle-range", + "chartType": "Range Area Chart", + "title": "Seattle, month by month", + "subtitle": "Average daily high and low temperature, °F, 1991–2020 normals", + "source": "NOAA climate normals", + "rows": 12 + }, + { + "id": "olympic-bump", + "chartType": "Bump Chart", + "title": "Four Games, four different stories", + "subtitle": "Rank in the Summer Olympics medal table, 2012–2024", + "source": "IOC medal tables", + "rows": 16 + }, + { + "id": "population-stream", + "chartType": "Streamgraph", + "title": "Where the world's people are", + "subtitle": "Population by region, 1950–2020, millions", + "source": "UN World Population Prospects 2022", + "rows": 25 + }, + { + "id": "browser-pie", + "chartType": "Pie Chart", + "title": "Chrome holds two-thirds of the desktop market", + "subtitle": "Desktop browser share, 2024, per cent", + "source": "StatCounter (approx. 2024)", + "rows": 5 + }, + { + "id": "mobile-donut", + "chartType": "Donut Chart", + "title": "Mobile OS market share, 2024", + "subtitle": "", + "source": "StatCounter (approx. 2024)", + "rows": 3 + }, + { + "id": "seattle-rose", + "chartType": "Rose Chart", + "title": "Seattle monthly rainfall (mm)", + "subtitle": "", + "source": "NOAA climate normals", + "rows": 12 + }, + { + "id": "stock-candle", + "chartType": "Candlestick Chart", + "title": "Two weeks of a stock going nowhere", + "subtitle": "Daily open, high, low and close, 2–12 January 2024", + "source": "Illustrative daily prices", + "rows": 9 + }, + { + "id": "temp-heatmap", + "chartType": "Heatmap", + "title": "Average monthly temperature", + "subtitle": "°C, climate normals, four cities", + "source": "Climate normals (approx.)", + "rows": 48 + }, + { + "id": "renewable-kpi", + "chartType": "KPI Card", + "title": "Renewables supply 30% of the world's electricity", + "subtitle": "Share of global electricity generation, 2023, against a 45% target", + "source": "Our World in Data / Ember", + "rows": 1 + }, + { + "id": "renewable-bullet", + "chartType": "Bullet Chart", + "title": "Every country is short of its renewable target", + "subtitle": "Renewable share of electricity, 2023, per cent, against national targets", + "source": "Our World in Data / Ember (targets illustrative)", + "rows": 5 + }, + { + "id": "cities-map", + "chartType": "Map", + "title": "World's largest cities (metro population)", + "subtitle": "", + "source": "UN / city statistics (approx.)", + "rows": 10 + }, + { + "id": "release-gantt", + "chartType": "Gantt Chart", + "title": "Software release schedule", + "subtitle": "", + "source": "Illustrative project plan", + "rows": 5 + }, + { + "id": "kpi-sparkline", + "chartType": "Sparkline", + "title": "Monthly KPIs", + "subtitle": "Twelve-month trend and latest value", + "source": "Illustrative company metrics", + "rows": 36 + }, + { + "id": "gapminder-bubble", + "chartType": "Scatter Plot", + "title": "Money buys years, up to a point", + "subtitle": "Life expectancy against GDP per capita, 2018; bubble area is population", + "source": "Gapminder / World Bank (2018)", + "rows": 15 + }, + { + "id": "anscombe", + "chartType": "Regression", + "title": "Anscombe's Quartet — same stats, different shapes", + "subtitle": "", + "source": "F. J. Anscombe (1973)", + "rows": 44 + }, + { + "id": "temp-anomaly", + "chartType": "Bar Chart", + "title": "Global temperature anomaly by decade", + "subtitle": "°C against the 1951–1980 average", + "source": "NASA GISTEMP (approx. decadal means)", + "rows": 9 + }, + { + "id": "us-unemployment", + "chartType": "Line Chart", + "title": "American unemployment", + "subtitle": "Annual rate, per cent of the labour force, 2000–2023", + "source": "US Bureau of Labor Statistics", + "rows": 24 + }, + { + "id": "titanic", + "chartType": "Grouped Bar Chart", + "title": "Titanic survival rate by class and sex", + "subtitle": "", + "source": "Encyclopedia Titanica passenger records", + "rows": 6 + }, + { + "id": "diamonds", + "chartType": "Scatter Plot", + "title": "Diamonds — carat vs price", + "subtitle": "", + "source": "ggplot2 \"diamonds\" (sample)", + "rows": 20 + }, + { + "id": "happiness", + "chartType": "Scatter Plot", + "title": "World Happiness vs income per capita (2023)", + "subtitle": "", + "source": "World Happiness Report 2023 / World Bank", + "rows": 12 + }, + { + "id": "sunspots", + "chartType": "Line Chart", + "title": "Sunspot number, 2000–2023", + "subtitle": "", + "source": "SILSO / Royal Observatory of Belgium (approx.)", + "rows": 24 + }, + { + "id": "marathon-wr", + "chartType": "Line Chart", + "title": "Men's marathon world record, 1908–2023 (minutes)", + "subtitle": "", + "source": "World Athletics record progression", + "rows": 16 + }, + { + "id": "ev-share", + "chartType": "Line Chart", + "title": "Electric cars as a share of new sales", + "subtitle": "Norway, China, Germany and the United States, 2018–2023, per cent of new car sales", + "source": "Our World in Data / IEA (approx.)", + "rows": 16 + }, + { + "id": "internet-users", + "chartType": "Area Chart", + "title": "Share of the world online, 1995–2023 (%)", + "subtitle": "", + "source": "Our World in Data / ITU", + "rows": 8 + }, + { + "id": "big-mac", + "chartType": "Bar Chart", + "title": "The price of a Big Mac", + "subtitle": "2023, converted to US dollars at market exchange rates", + "source": "The Economist Big Mac index (approx. 2023)", + "rows": 12 + }, + { + "id": "oecd-unemployment-facet", + "chartType": "Line Chart", + "title": "Out of work", + "subtitle": "Unemployment rate, %, 2000–2022", + "source": "OECD Labour Force Statistics (harmonised unemployment rate)", + "rows": 24 + }, + { + "id": "spending-quintile", + "chartType": "Stacked Bar Chart", + "title": "Where each income group's money goes", + "subtitle": "Share of annual household spending, by income quintile", + "source": "US Bureau of Labor Statistics, Consumer Expenditure Survey (approx. 2022)", + "rows": 25 + }, + { + "id": "trust-likert", + "chartType": "Stacked Bar Chart", + "title": "Confidence in US institutions", + "subtitle": "% of adults expressing each level of confidence", + "source": "Pew Research Center / Gallup confidence-in-institutions series (approx. 2023)", + "rows": 20 + }, + { + "id": "fed-funds-step", + "chartType": "Line Chart", + "title": "Federal funds target rate", + "subtitle": "Upper bound at year end, %", + "source": "US Federal Reserve, FOMC target range (year-end upper bound)", + "rows": 10 + }, + { + "id": "renewables-projection", + "chartType": "Line Chart", + "title": "Global renewable capacity", + "subtitle": "Gigawatts installed, observed to 2023 and projected to 2030", + "source": "IEA Renewables market report (approx. figures)", + "rows": 9 + }, + { + "id": "earnings-education", + "chartType": "Grouped Bar Chart", + "title": "Median weekly earnings by education and sex, 2023", + "subtitle": "US dollars, full-time wage and salary workers", + "source": "US Bureau of Labor Statistics, usual weekly earnings (2023 annual)", + "rows": 10 + }, + { + "id": "electricity-mix-area", + "chartType": "Area Chart", + "title": "Where the power comes from", + "subtitle": "World electricity generation by source, % of total", + "source": "IEA / Ember global electricity review (approx. shares)", + "rows": 24 + }, + { + "id": "state-unemployment", + "chartType": "Choropleth", + "title": "Unemployment rate by state, 2023", + "subtitle": "Annual average, % of the civilian labour force", + "source": "US Bureau of Labor Statistics, Local Area Unemployment Statistics (2023 annual)", + "rows": 50 + }, + { + "id": "causes-death", + "chartType": "Bar Chart", + "title": "What Americans die of", + "subtitle": "Leading causes of death, United States, 2022, thousands of deaths", + "source": "CDC / National Center for Health Statistics, leading causes of death (approx. 2022)", + "rows": 10 + }, + { + "id": "state-jobless", + "chartType": "Bar Chart", + "title": "Unemployment by state", + "subtitle": "Annual average unemployment rate, 50 US states, 2023, per cent", + "source": "US Bureau of Labor Statistics, Local Area Unemployment Statistics (2023 annual)", + "rows": 50 + }, + { + "id": "oecd-facet-16", + "chartType": "Line Chart", + "title": "Sixteen labour markets, one shock", + "subtitle": "Harmonised unemployment rate, selected OECD economies, 2000–2023, per cent", + "source": "OECD Labour Force Statistics (harmonised unemployment rate, approx.)", + "rows": 80 + }, + { + "id": "temp-uncertainty", + "chartType": "Range Area Chart", + "title": "The record gets more certain as it gets warmer", + "subtitle": "Global mean temperature anomaly against 1961–1990, with 95% confidence interval, °C", + "source": "Met Office Hadley Centre / UEA CRU, HadCRUT5 global annual anomaly vs 1961–1990 (approx.)", + "rows": 10 + } +] diff --git a/site/src/playground/theme-lab-assets/_headlines.json b/site/src/playground/theme-lab-assets/_headlines.json new file mode 100644 index 00000000..98572423 --- /dev/null +++ b/site/src/playground/theme-lab-assets/_headlines.json @@ -0,0 +1,188 @@ +{ + "_readme": [ + "Canonical headline text for the theme lab. The SAME title and subtitle strings are", + "stamped onto both columns: `scripts/dump-flint-specs.ts` writes them into every", + "`.flint.json` baseline (Flint itself has no title support — ChartAssemblyInput", + "has no title field — so a consumer has to post-process the spec), and each", + "hand-authored `..json` carries the identical strings.", + "", + "Holding the words constant is the point: it removes the confound where a bespoke", + "redesign looks better merely because it has a headline and the baseline does not.", + "What differs between the two columns is typography, anchoring, spacing and colour —", + "never the information.", + "", + "Cases with no entry here fall back to the corpus title from new-case-preview-data.ts", + "and get no subtitle. Duplicating strings that also live in the corpus is deliberate:", + "the theme lab owns its own text so specs can be tuned without touching the gallery." + ], + "headlines": { + "ev-share": { + "title": "Electric cars as a share of new sales", + "subtitle": "Norway, China, Germany and the United States, 2018–2023, per cent of new car sales" + }, + "renewable-kpi": { + "title": "Renewables supply 30% of the world's electricity", + "subtitle": "Share of global electricity generation, 2023, against a 45% target" + }, + "renewable-bullet": { + "title": "Every country is short of its renewable target", + "subtitle": "Renewable share of electricity, 2023, per cent, against national targets" + }, + "stock-candle": { + "title": "Two weeks of a stock going nowhere", + "subtitle": "Daily open, high, low and close, 2–12 January 2024" + }, + "us-pyramid": { + "title": "A pyramid that is no longer a pyramid", + "subtitle": "United States population by age and sex, 2020, millions" + }, + "co2-lollipop": { + "title": "Emissions per person run from 37 tonnes to 2", + "subtitle": "Carbon dioxide emissions per capita, 2022, tonnes" + }, + "browser-pie": { + "title": "Chrome holds two-thirds of the desktop market", + "subtitle": "Desktop browser share, 2024, per cent" + }, + "gdp-bartable": { + "title": "America and China lap the field", + "subtitle": "Gross domestic product, 2023, trillion US dollars" + }, + "population-waterfall": { + "title": "Asia added more people than the world held in 1950", + "subtitle": "Contribution to world population growth by region, 1950–2020, millions" + }, + "lifeexp-dumbbell": { + "title": "Women outlive men everywhere, but not by the same margin", + "subtitle": "Life expectancy at birth by sex, 2021, years" + }, + "faithful-hist": { + "title": "Old Faithful eruption durations", + "subtitle": "Two clusters, not one: short eruptions near 2 min and long ones near 4 min" + }, + "penguins-violin": { + "title": "Body mass by penguin species", + "subtitle": "Kernel density with all observations overlaid; horizontal rule marks the median" + }, + "auto-mpg": { + "title": "Power against fuel economy", + "subtitle": "Ordinary least squares fit; each point is one car model" + }, + "life-expectancy": { + "title": "Two decades of longer lives", + "subtitle": "Life expectancy at birth, years, 2000 and 2021" + }, + "seattle-range": { + "title": "Seattle, month by month", + "subtitle": "Average daily high and low temperature, °F, 1991–2020 normals" + }, + "gapminder-bubble": { + "title": "Money buys years, up to a point", + "subtitle": "Life expectancy against GDP per capita, 2018; bubble area is population" + }, + "driving": { + "title": "Driving shifts into reverse", + "subtitle": "Miles driven per person against the price of a gallon of gas, United States, 1956–2010" + }, + "olympic-bump": { + "title": "Four Games, four different stories", + "subtitle": "Rank in the Summer Olympics medal table, 2012–2024" + }, + "population-stream": { + "title": "Where the world's people are", + "subtitle": "Population by region, 1950–2020, millions" + }, + "keeling": { + "title": "Keeling Curve", + "subtitle": "Atmospheric CO₂ at Mauna Loa, annual mean, parts per million" + }, + "temp-anomaly": { + "title": "Global temperature anomaly by decade", + "subtitle": "°C against the 1951–1980 average" + }, + "us-unemployment": { + "title": "American unemployment", + "subtitle": "Annual rate, per cent of the labour force, 2000–2023" + }, + "renewables-projection": { + "title": "Global renewable capacity", + "subtitle": "Gigawatts installed, observed to 2023 and projected to 2030" + }, + "big-mac": { + "title": "The price of a Big Mac", + "subtitle": "2023, converted to US dollars at market exchange rates" + }, + "oecd-unemployment-facet": { + "title": "Out of work", + "subtitle": "Unemployment rate, %, 2000–2022" + }, + "electricity-mix-area": { + "title": "Where the power comes from", + "subtitle": "World electricity generation by source, % of total" + }, + "penguins": { + "title": "Flipper length versus body mass in three penguin species", + "subtitle": "Palmer Archipelago, Antarctica; n = 33" + }, + "penguins-box": { + "title": "Body mass of three Pygoscelis species", + "subtitle": "Boxes show median and interquartile range; whiskers span the full sample; points are individual birds" + }, + "exam-ecdf": { + "title": "Empirical distribution of exam scores", + "subtitle": "n = 30; circles mark individual scores" + }, + "population": { + "title": "Most populous countries, 2023", + "subtitle": "Population in millions" + }, + "earnings-education": { + "title": "Median weekly earnings by education and sex, 2023", + "subtitle": "US dollars, full-time wage and salary workers" + }, + "spending-quintile": { + "title": "Where each income group's money goes", + "subtitle": "Share of annual household spending, by income quintile" + }, + "population-region": { + "title": "World population by region", + "subtitle": "1950–2020, millions of people" + }, + "trust-likert": { + "title": "Confidence in US institutions", + "subtitle": "% of adults expressing each level of confidence" + }, + "state-unemployment": { + "title": "Unemployment rate by state, 2023", + "subtitle": "Annual average, % of the civilian labour force" + }, + "temp-heatmap": { + "title": "Average monthly temperature", + "subtitle": "°C, climate normals, four cities" + }, + "fed-funds-step": { + "title": "Federal funds target rate", + "subtitle": "Upper bound at year end, %" + }, + "kpi-sparkline": { + "title": "Monthly KPIs", + "subtitle": "Twelve-month trend and latest value" + }, + "causes-death": { + "title": "What Americans die of", + "subtitle": "Leading causes of death, United States, 2022, thousands of deaths" + }, + "state-jobless": { + "title": "Unemployment by state", + "subtitle": "Annual average unemployment rate, 50 US states, 2023, per cent" + }, + "oecd-facet-16": { + "title": "Sixteen labour markets, one shock", + "subtitle": "Harmonised unemployment rate, selected OECD economies, 2000–2023, per cent" + }, + "temp-uncertainty": { + "title": "The record gets more certain as it gets warmer", + "subtitle": "Global mean temperature anomaly against 1961–1990, with 95% confidence interval, °C" + } + } +} diff --git a/site/src/playground/theme-lab-assets/_themes.json b/site/src/playground/theme-lab-assets/_themes.json new file mode 100644 index 00000000..8f3451f9 --- /dev/null +++ b/site/src/playground/theme-lab-assets/_themes.json @@ -0,0 +1,101 @@ +{ + "order": ["nyt", "economist", "nature", "mckinsey", "datawrapper", "powerbi"], + "themes": { + "nyt": { + "label": "NYT", + "alias": "narrative-editorial", + "surface": "#ffffff", + "ink": "#121212", + "accent": "#c2352b", + "swatches": ["#c2352b", "#2f6b9a", "#d9a441", "#4a8b6f", "#7f6a9e", "#9e9e9e"], + "intent": "One chart, one idea. Typography carries the hierarchy, the frame gets out of the way, and a single accent colour does the pointing that a caption would otherwise have to do.", + "signature": [ + "Serif headline flush left, sans-serif everything else", + "No frame: horizontal gridlines only, black x baseline kept", + "Legend replaced by direct series labels where the data allows", + "One editorial accent; other series fall back to grey", + "Y-axis title rotated flat above the axis as a bare unit" + ] + }, + "economist": { + "label": "Economist", + "alias": "compact-editorial", + "surface": "#ffffff", + "ink": "#121317", + "accent": "#e3120b", + "swatches": ["#006ba2", "#3ebcd2", "#379a8b", "#ebb434", "#b4ba39", "#9a607f"], + "intent": "A branded house style that has to survive being printed a hundred times a week. Fixed identity, dense information, zero per-chart art direction.", + "signature": [ + "Red masthead rule top-left; bold headline, grey deck below", + "Value axis moved to the top; horizontal gridlines only", + "Hard baseline, thin bars, wide gutters", + "Fixed house palette used in a fixed order", + "9.5pt labels throughout — sized for a two-column print grid" + ] + }, + "nature": { + "label": "Nature", + "alias": "scientific-publication", + "surface": "#ffffff", + "ink": "#000000", + "accent": "#0072b2", + "swatches": ["#0072b2", "#e69f00", "#009e73", "#cc79a7", "#56b4e9", "#d55e00"], + "intent": "A figure panel that must be readable at 89 mm column width, in greyscale, by a colour-blind reviewer, and defensible in a methods section.", + "signature": [ + "Column-width geometry: 8.5pt type, hairlines, tight padding", + "Black L-shaped spines, outward ticks, no gridlines", + "Okabe–Ito palette; colour is never the only cue", + "Terse title — the caption lives in the body text", + "Units mandatory in every axis title" + ] + }, + "mckinsey": { + "label": "McKinsey", + "alias": "executive-briefing", + "surface": "#ffffff", + "ink": "#051c2c", + "accent": "#2251ff", + "swatches": ["#2251ff", "#b3c0c9", "#d3dce1", "#8fa0ab", "#00a9f4", "#051c2c"], + "intent": "The chart is evidence for a claim someone is about to make out loud. Everything that is not the claim is greyed out or deleted.", + "signature": [ + "Value axis deleted — numbers printed on the marks", + "Exactly one series in brand blue; the rest grey", + "No gridlines, ticks or axis titles", + "Horizontal bars, sorted, labels reading left to right", + "Deep-navy ink and generous whitespace" + ] + }, + "datawrapper": { + "label": "Datawrapper", + "alias": "accessible-civic", + "surface": "#ffffff", + "ink": "#333333", + "accent": "#18a1cd", + "swatches": ["#18a1cd", "#e2a233", "#c04a4a", "#2d8659", "#7e5aa2", "#6b6b6b"], + "intent": "A public-facing chart that will be read on a phone and republished by a local newsroom. Legibility is the whole brief.", + "signature": [ + "11pt type floor so it survives mobile downscaling", + "High-contrast ink, light dashed gridlines, no frame", + "Explicit colour domain so a category keeps its hue across a story", + "Legend on top with large square symbols", + "Footer hairline closing the chart block" + ] + }, + "powerbi": { + "label": "Power BI", + "alias": "product-dashboard", + "surface": "#1b1a19", + "ink": "#f3f2f1", + "accent": "#118dff", + "swatches": ["#118dff", "#12239e", "#e66c37", "#6b007b", "#e044a7", "#d9b300"], + "intent": "A tile in a grid of tiles that refreshes every hour. Geometry has to stay stable as the data changes, and the whole thing has to work in dark mode.", + "signature": [ + "Dark card surface; ink and grid tuned for #1B1A19", + "Compact 9.5pt Segoe UI — the tile is small on purpose", + "Dashed low-contrast gridlines; marks stay dominant", + "Legend in a fixed position as a reusable convention", + "Status colours reserved for thresholds" + ] + } + } +} diff --git a/site/src/playground/theme-lab-assets/_themespecs.json b/site/src/playground/theme-lab-assets/_themespecs.json new file mode 100644 index 00000000..65c42064 --- /dev/null +++ b/site/src/playground/theme-lab-assets/_themespecs.json @@ -0,0 +1,1000 @@ +{ + "$comment": "ThemeSpecs for the six Theme Lab design languages, expressed in themespec.v6. Values are measured from the hand-authored redesigns, not invented. `coverage` records, per case, whether v6 is sufficient for a compiler to reach the hand-authored result.", + "schema": "https://flint.dev/schema/themespec/v12", + "themes": { + "nyt": { + "id": "nyt", + "label": "New York Times", + "ink": { + "surface": { + "source": "host" + }, + "text": { + "primary": "#121212", + "secondary": "#6b6b6b", + "muted": "#8a8a8a", + "inverse": "#ffffff" + }, + "structure": { + "grid": "#e4e4e4", + "axis": "#121212", + "rule": "#121212" + }, + "series": { + "single": "#2f6b9a", + "categorical": [ + "#2f6b9a", + "#c2352b", + "#4a8b6f", + "#7f6a9e", + "#d9a441" + ], + "diverging": { + "stops": [ + "#2f6b9a", + "#8fb4cc", + "#efece5", + "#dd9a86", + "#c2352b" + ], + "neutral": "#efece5", + "space": "lab", + "endpointsAgainstSurface": true, + "consumption": "interpolate" + }, + "overflow": "#9e9e9e", + "status": { + "positive": "#2f6b9a", + "negative": "#c2352b", + "neutral": "#9e9e9e" + }, + "selection": { + "signed": "status", + "statusUse": "anySigned" + } + }, + "accent": "#c2352b" + }, + "type": { + "minSize": 8, + "headline": { + "family": "Georgia, serif", + "size": "text.400", + "weight": "bold", + "color": "#121212" + }, + "deck": { + "family": "Georgia, serif", + "size": "text.200", + "color": "#6b6b6b" + }, + "axisLabel": { + "family": "Helvetica, Arial, sans-serif", + "size": "text.100" + }, + "valueLabel": { + "family": "Helvetica, Arial, sans-serif", + "size": "text.100", + "weight": "bold" + } + }, + "structure": { + "axis": { + "categorical": { + "line": "full", + "ticks": "omit", + "tickLabels": "sparse" + }, + "measure": { + "line": "omit", + "ticks": "omit", + "tickDensity": "sparse" + } + }, + "grid": { + "measure": "quiet", + "category": "omit", + "style": "solid" + }, + "frame": "omit", + "baseline": "full" + }, + "marks": { + "bandFraction": 0.72, + "strokeWeight": 2.4, + "strokeCap": "round", + "strokeJoin": "round", + "zOrder": "summaryOverData", + "redundantEncoding": "whenNeeded", + "redundantChannels": [ + "dash" + ] + }, + "labels": { + "truncation": "never", + "flush": true + }, + "legend": { + "show": "always", + "placement": [ + "seriesEnd", + "top" + ], + "title": "omit", + "suppressWhenValuesPrinted": false + }, + "dataLabels": { + "show": "always", + "placement": "atMark", + "inkMode": "contrastWithMark" + }, + "annotation": { + "axisTitles": "omit", + "axisTitlePlacement": "flatAboveAxis", + "unit": "lastTick", + "pointEmphasis": "endpoints", + "numberFormat": { + "precision": "auto", + "thousands": "suffix" + } + }, + "layout": { + "density": "normal", + "titleBlock": { + "anchor": "start" + } + } + }, + "economist": { + "id": "economist", + "label": "The Economist", + "ink": { + "surface": { + "source": "host" + }, + "text": { + "primary": "#121317", + "secondary": "#54585a", + "muted": "#8b9196" + }, + "structure": { + "grid": "#c9d3da", + "axis": "#121317", + "rule": "#c9d3da" + }, + "series": { + "single": "#006ba2", + "categorical": [ + "#3f5661", + "#a1655a", + "#006ba2", + "#7ba7b8", + "#3ebcd2", + "#c8b88a" + ], + "diverging": { + "stops": [ + "#006ba2", + "#7ba7b8", + "#e9e5dc", + "#c8967a", + "#a1655a" + ], + "neutral": "#e9e5dc", + "space": "lab", + "endpointsAgainstSurface": true, + "consumption": "interpolate" + }, + "status": { + "positive": "#006ba2", + "negative": "#e3120b", + "neutral": "#b8c4cc" + }, + "selection": { + "signed": "status", + "statusUse": "anySigned" + } + }, + "accent": "#e3120b" + }, + "type": { + "minSize": 8, + "headline": { + "family": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "size": "text.300", + "weight": "bold" + }, + "deck": { + "size": "text.200", + "color": "#54585a" + }, + "axisLabel": { + "size": "text.100" + } + }, + "structure": { + "axis": { + "categorical": { + "line": "full", + "ticks": "omit" + }, + "measure": { + "line": "omit", + "ticks": "omit", + "placement": "default" + } + }, + "grid": { + "measure": "quiet", + "category": "omit", + "style": "solid" + }, + "frame": "omit", + "baseline": "full" + }, + "marks": { + "bandFraction": 0.68, + "strokeWeight": 1.6, + "interval": { + "fillOpacity": 0.22, + "edge": "quiet", + "inkSource": "sameAsCentral" + } + }, + "labels": { + "truncation": "never", + "angle": "auto" + }, + "legend": { + "show": "always", + "placement": [ + "seriesEnd", + "top" + ], + "direction": "horizontal", + "title": "omit" + }, + "dataLabels": { + "show": "whenTheyFit", + "placement": "atMark" + }, + "annotation": { + "axisTitles": "omit", + "unit": "everyTick" + }, + "furniture": [ + { + "kind": "mastheadTab", + "anchor": "topLeft", + "color": "#e3120b", + "width": 26, + "height": 3 + } + ], + "layout": { + "density": "compact", + "titleBlock": { + "anchor": "start" + } + }, + "variants": [ + { + "when": { + "markChannel": "length" + }, + "then": { + "structure": { + "axis": { + "measure": { + "placement": "opposite" + } + } + } + }, + "because": "Measured: big-mac, causes-death and state-jobless all carry the measure axis opposite (3 of 3 bar charts). On a banded chart the far edge is where a reader enters." + }, + { + "when": { + "isPartToWhole": true + }, + "then": { + "structure": { + "axis": { + "measure": { + "placement": "opposite" + } + } + } + }, + "because": "Measured: electricity-mix-area puts y on the right, seattle-range does not. Both are area marks, so markChannel cannot separate them; isPartToWhole can. On a pie the policy is inert." + } + ] + }, + "nature": { + "id": "nature", + "label": "Nature", + "ink": { + "surface": { + "source": "host" + }, + "text": { + "primary": "#000000", + "secondary": "#000000" + }, + "structure": { + "axis": "#000000", + "grid": "#00000000", + "frame": "#000000" + }, + "series": { + "single": "#0072b2", + "categorical": [ + "#0072b2", + "#e69f00", + "#009e73", + "#cc79a7", + "#56b4e9", + "#d55e00" + ], + "diverging": { + "stops": [ + "#0072b2", + "#83b9db", + "#ffffff", + "#eba06a", + "#d55e00" + ], + "neutral": "#ffffff", + "space": "lab", + "endpointsAgainstSurface": false, + "consumption": "interpolate" + }, + "overflow": "#999999", + "selection": { + "partToWhole": "categorical" + } + }, + "accent": "#000000" + }, + "type": { + "minSize": 8.5, + "headline": { + "family": "Arial, Helvetica, sans-serif", + "size": "text.200", + "weight": "bold" + }, + "deck": { + "size": "text.100", + "case": "asIs" + }, + "axisLabel": { + "family": "Arial, Helvetica, sans-serif", + "size": "text.100" + }, + "axisTitle": { + "family": "Arial, Helvetica, sans-serif", + "size": "text.100" + } + }, + "structure": { + "axis": { + "categorical": { + "line": "full", + "ticks": "full", + "tickLength": "long", + "tickDirection": "outward" + }, + "measure": { + "line": "full", + "ticks": "full", + "tickLength": "long", + "tickDirection": "outward" + } + }, + "grid": { + "measure": "omit", + "category": "omit" + }, + "frame": "omit", + "baseline": "quiet" + }, + "marks": { + "bandFraction": 0.55, + "strokeWeight": 1.2, + "separator": { + "presence": "hairline", + "source": "surface", + "width": 0.5 + }, + "point": { + "presence": "full", + "size": 26, + "fill": "solid", + "halo": { + "presence": "hairline", + "width": 0.6 + } + }, + "interval": { + "fillOpacity": 0.25, + "edge": "omit", + "inkSource": "sameAsCentral" + }, + "summary": { + "fill": "omit", + "outline": "full", + "centralRule": "emphasised", + "widthFraction": 0.4 + }, + "observations": { + "expose": "always", + "maxRows": 400 + }, + "zOrder": "summaryUnderData", + "redundantEncoding": "always", + "redundantChannels": [ + "shape" + ] + }, + "labels": { + "truncation": "never" + }, + "legend": { + "show": "always", + "placement": [ + "right", + "inside" + ], + "title": "whenAmbiguous", + "suppressWhenAxisNames": true + }, + "dataLabels": { + "show": "whenTheyFit", + "placement": "outsideMark" + }, + "annotation": { + "axisTitles": "always", + "axisTitlePlacement": "rotated", + "unitsInAxisTitle": true, + "statistics": { + "show": [ + "n", + "r2", + "slope" + ], + "placement": "panel" + } + }, + "layout": { + "density": "compact", + "targetWidth": 252, + "titleBlock": { + "anchor": "start" + } + } + }, + "mckinsey": { + "id": "mckinsey", + "label": "McKinsey", + "ink": { + "surface": { + "source": "host" + }, + "text": { + "primary": "#051c2c", + "secondary": "#5a6872", + "muted": "#8a969d" + }, + "structure": { + "axis": "#051c2c", + "rule": "#d3dce1" + }, + "series": { + "single": "#051c2c", + "categorical": [ + "#051c2c", + "#2251ff", + "#00a9f4", + "#00cfb4", + "#8c9ba5" + ], + "sequential": { + "stops": [ + "#eef3f8", + "#cfdcea", + "#9db8d2", + "#5b82ab", + "#051c2c" + ], + "space": "lab", + "endpointsAgainstSurface": true, + "consumption": "interpolate", + "$note": "Stated light-to-dark. browser-pie.mckinsey samples the same navy ramp in reverse for a categorical part-to-whole; temp-heatmap.mckinsey interpolates it. One ramp, two consumptions." + }, + "selection": { + "partToWhole": "sequentialRamp", + "signed": "sequential" + } + }, + "accent": "#2251ff" + }, + "type": { + "minSize": 9, + "headline": { + "family": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "size": "text.300", + "weight": "bold" + }, + "axisLabel": { + "size": "text.200" + }, + "valueLabel": { + "size": "text.200", + "weight": "semibold", + "color": "#051c2c" + } + }, + "structure": { + "axis": { + "categorical": { + "line": "omit", + "ticks": "omit" + }, + "measure": { + "line": "omit", + "ticks": "omit" + } + }, + "grid": { + "measure": "omit", + "category": "omit" + }, + "frame": "omit", + "baseline": "full" + }, + "marks": { + "bandFraction": 0.62, + "strokeWeight": 2, + "connector": { + "presence": "hairline", + "weight": 0.8 + }, + "separator": { + "presence": "hairline", + "source": "surface", + "width": 0.6 + } + }, + "labels": { + "truncation": "never", + "angle": "horizontal" + }, + "legend": { + "show": "always", + "placement": [ + "seriesEnd", + "inline", + "top" + ], + "direction": "horizontal", + "title": "omit", + "suppressWhenValuesPrinted": true + }, + "dataLabels": { + "show": "always", + "placement": "column", + "inkMode": "contrastWithMark" + }, + "annotation": { + "axisTitles": "omit", + "numberFormat": { + "precision": "integer", + "thousands": "separator" + } + }, + "layout": { + "density": "airy", + "titleBlock": { + "anchor": "start" + } + } + }, + "datawrapper": { + "id": "datawrapper", + "label": "Datawrapper", + "ink": { + "surface": { + "source": "host" + }, + "text": { + "primary": "#333333", + "secondary": "#666666", + "muted": "#999999" + }, + "structure": { + "grid": "#dcdcdc", + "rule": "#dcdcdc", + "axis": "#333333" + }, + "series": { + "single": "#18a1cd", + "categorical": [ + "#18a1cd", + "#e2a233", + "#c04a4a", + "#2d8659", + "#7e5aa2" + ], + "sequential": { + "stops": [ + "#dceef6", + "#a9d3e6", + "#6aabcc", + "#2f7fa8", + "#0b5c82" + ], + "space": "lab", + "consumption": "quantize", + "quantizeCount": 5 + }, + "diverging": { + "stops": [ + "#2f7fa8", + "#a9d3e6", + "#f0ece4", + "#e8ac70", + "#c04a4a" + ], + "neutral": "#f0ece4", + "space": "lab", + "endpointsAgainstSurface": true, + "consumption": "quantize", + "quantizeCount": 5 + }, + "selection": {} + }, + "accent": "#18a1cd" + }, + "type": { + "minSize": 11, + "headline": { + "family": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "size": "text.300", + "weight": "bold" + }, + "axisLabel": { + "size": "text.200" + }, + "keyLabel": { + "size": "text.200" + } + }, + "structure": { + "axis": { + "categorical": { + "line": "full", + "ticks": "omit" + }, + "measure": { + "line": "omit", + "ticks": "omit" + } + }, + "grid": { + "measure": "quiet", + "category": "omit", + "style": "dashed" + }, + "frame": "omit", + "baseline": "quiet" + }, + "marks": { + "bandFraction": 0.66, + "separator": { + "presence": "hairline", + "source": "surface", + "width": 1.5 + }, + "connector": { + "presence": "hairline", + "weight": 1 + } + }, + "labels": { + "truncation": "never" + }, + "legend": { + "show": "always", + "placement": [ + "top" + ], + "direction": "horizontal", + "title": "omit" + }, + "dataLabels": { + "show": "whenTheyFit", + "placement": "outsideMark" + }, + "annotation": { + "axisTitles": "omit", + "unit": "lastTick", + "numberFormat": { + "precision": "auto" + } + }, + "furniture": [ + { + "kind": "footerRule", + "anchor": "bottomLeft", + "color": "#dcdcdc", + "height": 1 + } + ], + "interaction": { + "tooltipFormat": "matchKey" + }, + "layout": { + "density": "normal", + "targetWidth": 300, + "titleBlock": { + "anchor": "start" + } + } + }, + "powerbi": { + "id": "powerbi", + "label": "Power BI", + "ink": { + "surface": { + "source": "house", + "canvas": "#1b1a19", + "plot": "#1b1a19", + "panel": "#252423" + }, + "text": { + "primary": "#f3f2f1", + "secondary": "#c8c6c4", + "muted": "#a19f9d", + "inverse": "#1b1a19" + }, + "structure": { + "grid": "#3b3a39", + "axis": "#3b3a39", + "rule": "#3b3a39" + }, + "series": { + "single": "#118dff", + "categorical": [ + "#118dff", + "#12239e", + "#e66c37", + "#6b007b", + "#e044a7", + "#744ec2" + ], + "diverging": { + "stops": [ + "#118dff", + "#5aa9f0", + "#4a4948", + "#e08a4a", + "#d64550" + ], + "neutral": "#4a4948", + "space": "lab", + "endpointsAgainstSurface": true, + "consumption": "interpolate" + }, + "status": { + "positive": "#22b14c", + "negative": "#e66c37", + "neutral": "#a19f9d" + }, + "selection": { + "signed": "diverging", + "statusUse": "thresholdOnly", + "redundantWithFacet": "single" + } + }, + "accent": "#118dff" + }, + "type": { + "minSize": 8, + "headline": { + "family": "'Segoe UI', system-ui, sans-serif", + "size": "text.200", + "weight": "semibold", + "color": "#f3f2f1" + }, + "display": { + "family": "'Segoe UI', system-ui, sans-serif", + "size": "text.hero900", + "weight": "semibold" + }, + "axisLabel": { + "family": "'Segoe UI', system-ui, sans-serif", + "size": "text.100", + "color": "#c8c6c4" + }, + "keyLabel": { + "size": "text.100", + "color": "#c8c6c4" + } + }, + "structure": { + "axis": { + "categorical": { + "line": "omit", + "ticks": "omit" + }, + "measure": { + "line": "omit", + "ticks": "omit", + "tickDensity": "sparse" + } + }, + "grid": { + "measure": "quiet", + "category": "omit", + "style": "dashed" + }, + "frame": "omit", + "baseline": "quiet" + }, + "marks": { + "strokeWeight": 2.2, + "strokeCap": "square", + "minSize": 1.5, + "separator": { + "presence": "hairline", + "source": "surface", + "width": 1 + }, + "trailingFill": { + "presence": "quiet", + "opacity": 0.18 + }, + "reference": { + "presence": "full", + "style": "tick", + "label": true, + "weight": 2 + } + }, + "labels": { + "truncation": "never" + }, + "legend": { + "show": "always", + "placement": [ + "right", + "bottom" + ], + "title": "omit", + "gradientLength": 90, + "suppressWhenValuesPrinted": false + }, + "dataLabels": { + "show": "whenTheyFit", + "placement": "atMark", + "inkMode": "contrastWithMark" + }, + "annotation": { + "axisTitles": "omit", + "unit": "everyTick", + "pointEmphasis": "latest", + "numberFormat": { + "precision": "auto" + } + }, + "facets": { + "header": { + "presence": "full", + "style": "flushLabel", + "fieldTitle": "omit" + }, + "panelFrame": "omit", + "axisRepetition": "edgeOnly", + "preferredColumns": 4, + "sharedScale": "whenComparable" + }, + "layout": { + "density": "compact", + "titleBlock": { + "anchor": "start" + } + } + } + }, + "coverage": { + "$comment": "status: `full` = v6 is sufficient for a compiler; `partial` = reachable but one stated move is not derivable; `blocked` = a required move is outside the ThemeSpec by construction.", + "electricity-mix-area": { + "status": "blocked", + "notes": [ + "Colour assigned by domain meaning: coal and gas take dark slate and rust so the ramp reads dirty-to-clean. No compiler-resolvable signal distinguishes coal from wind. This is editorial knowledge, not a theme policy." + ] + }, + "gdp-bartable": { + "status": "blocked", + "notes": [ + "A two-panel hconcat is collapsed into one plot. §7 forbids the ThemeSpec from altering composition, and this redesign depends on it." + ] + }, + "us-pyramid": { + "status": "full", + "notes": [ + "Reclassified in iteration 11. 'Male' and 'Female' are titles over the two halves of the plot, not names attached to marks, so this case supports no legend placement at all - it is `annotation`/title territory. It had been counted as direct labelling only because the draft implementation derived the two words by filtering the data to the '75+' band. Fixing that removed Datawrapper's only series-end evidence." + ] + }, + "stock-candle": { + "status": "blocked", + "notes": [ + "A temporal scale is converted to ordinal to delete non-trading gaps. That changes what the x position means, so it is semantic." + ] + }, + "penguins-box": { + "status": "partial", + "notes": [ + "Whisker extent changed from 1.5×IQR to min-max. A statistical definition, not a treatment — correctly outside the ThemeSpec, but the redesign needs it." + ] + }, + "faithful-hist": { + "status": "partial", + "notes": [ + "Bin width stated rather than inferred. Binning changes the claim, so it is semantic." + ] + }, + "exam-ecdf": { + "status": "partial", + "notes": [ + "Curve extended to both edges of the score range so the function starts at 0 and reaches 1. A domain decision." + ] + }, + "driving": { + "status": "partial", + "notes": [ + "Zero baseline dropped. Zero inclusion is semantic — and NYT drops it here while forcing it in us-unemployment, so there is no house policy to encode." + ] + }, + "keeling": { + "status": "partial", + "notes": [ + "Zero baseline dropped, same reasoning." + ] + }, + "renewable-kpi": { + "status": "partial", + "notes": [ + "Flint emits '67% of 45' as a string literal; the redesign computes it. A template-quality fix, not a theming act." + ] + }, + "trust-likert": { + "status": "partial", + "notes": [ + "stackMode 'center' is a no-op when every row totals 100, so the chart is not the diverging Likert it declares. A chart-type capability gap." + ] + }, + "renewable-bullet": { + "status": "partial", + "notes": [ + "Qualitative bands are not comparable across rows; the redesign replaces them. A semantic repair." + ] + }, + "temp-heatmap": { + "status": "full", + "notes": [ + "The continuous-colour cluster, added in iteration 9. All six languages redesign the same heatmap, so the ramp policy is separated from the chart that asked for it.", + "Confirms that ranked `legend.placement` degrades on its own: NYT, Economist and McKinsey all rank `seriesEnd` first, which a continuous ramp cannot realize, so grounding falls through to `top` and every one of them keeps a boxed legend - without the theme naming an exception.", + "Forced two fields - `ink.series.selection.signed: sequential` (McKinsey) and `legend.suppressWhenValuesPrinted` (McKinsey again). The repeated appearance of `suppressWhen*` flags is what eventually exposed `keying` as a legend block, in iteration 10.", + "Nature's ramp neutral is #FFFFFF, which is also its surface. Dropping the cell stroke follows from comparing `ramp.neutral` to the resolved surface, so it needs no new field - but it is the first case where the MIDPOINT, not the endpoints, collides with the paper." + ] + }, + "state-unemployment": { + "status": "partial", + "notes": [ + "`consumption: quantize` with `quantizeCount: 5` reaches the banded scale and the swatch key. What it does not reach is the break FORMAT: at one significant figure the five bands read 2%, 3%, 3%, 4%, 4%, and the renderer then drops the collisions. Choosing the precision that makes breaks distinct is a numeric decision the ThemeSpec cannot make without seeing the domain." + ] + } + }, + "$evidence": "ink.series.sequential / .diverging are measured from every redesign in the lab that carries a continuous colour scale: state-unemployment.datawrapper (quantized sequential) and temp-heatmap in all six languages (diverging, except McKinsey). browser-pie.mckinsey samples a ramp categorically. temp-anomaly.nyt looks continuous but is nominal Below/Above average, so it is ink.series.status. McKinsey carries no diverging ramp because the house has no warm ink: on temp-heatmap it answers a signed measure with a single-hue sequential wash plus printed values, which is a measured house decision rather than a gap. Nature is the only language whose ramp does not hold its endpoints against the surface - its midpoint IS the surface, which is why its cells are left unstroked." +} diff --git a/site/src/playground/theme-lab-assets/anscombe.flint.json b/site/src/playground/theme-lab-assets/anscombe.flint.json new file mode 100644 index 00000000..87d425ac --- /dev/null +++ b/site/src/playground/theme-lab-assets/anscombe.flint.json @@ -0,0 +1,331 @@ +{ + "facet": { + "field": "Dataset", + "type": "nominal", + "sort": null + }, + "columns": 4, + "spec": { + "layer": [ + { + "mark": "circle", + "encoding": { + "x": { + "field": "X", + "type": "quantitative", + "scale": { + "zero": false + }, + "axis": { + "format": ",.12~g" + } + }, + "y": { + "field": "Y", + "type": "quantitative", + "scale": { + "zero": false + }, + "axis": { + "format": ",.12~g" + } + }, + "color": {}, + "size": {} + } + }, + { + "mark": { + "type": "line", + "color": "red" + }, + "transform": [ + { + "regression": "Y", + "on": "X" + } + ], + "encoding": { + "x": { + "field": "X", + "type": "quantitative", + "scale": { + "zero": false + }, + "axis": { + "format": ",.12~g" + } + }, + "y": { + "field": "Y", + "type": "quantitative", + "scale": { + "zero": false + }, + "axis": { + "format": ",.12~g" + } + } + } + } + ], + "encoding": {} + }, + "config": { + "view": { + "continuousWidth": 105, + "continuousHeight": 154, + "stroke": null + }, + "axisX": { + "labelLimit": 100, + "labelFontSize": 8, + "titleFontSize": 11, + "titleFontWeight": "normal", + "titleColor": "#666" + }, + "axisY": { + "labelFontSize": 8, + "titleFontSize": 11, + "titleFontWeight": "normal", + "titleColor": "#666" + }, + "legend": { + "labelFontSize": 9, + "titleFontSize": 9 + }, + "headerFacet": { + "labelLimit": 125 + }, + "facet": { + "spacing": 11 + } + }, + "data": { + "values": [ + { + "Dataset": "I", + "X": 10, + "Y": 8.04 + }, + { + "Dataset": "I", + "X": 8, + "Y": 6.95 + }, + { + "Dataset": "I", + "X": 13, + "Y": 7.58 + }, + { + "Dataset": "I", + "X": 9, + "Y": 8.81 + }, + { + "Dataset": "I", + "X": 11, + "Y": 8.33 + }, + { + "Dataset": "I", + "X": 14, + "Y": 9.96 + }, + { + "Dataset": "I", + "X": 6, + "Y": 7.24 + }, + { + "Dataset": "I", + "X": 4, + "Y": 4.26 + }, + { + "Dataset": "I", + "X": 12, + "Y": 10.84 + }, + { + "Dataset": "I", + "X": 7, + "Y": 4.82 + }, + { + "Dataset": "I", + "X": 5, + "Y": 5.68 + }, + { + "Dataset": "II", + "X": 10, + "Y": 9.14 + }, + { + "Dataset": "II", + "X": 8, + "Y": 8.14 + }, + { + "Dataset": "II", + "X": 13, + "Y": 8.74 + }, + { + "Dataset": "II", + "X": 9, + "Y": 8.77 + }, + { + "Dataset": "II", + "X": 11, + "Y": 9.26 + }, + { + "Dataset": "II", + "X": 14, + "Y": 8.1 + }, + { + "Dataset": "II", + "X": 6, + "Y": 6.13 + }, + { + "Dataset": "II", + "X": 4, + "Y": 3.1 + }, + { + "Dataset": "II", + "X": 12, + "Y": 9.13 + }, + { + "Dataset": "II", + "X": 7, + "Y": 7.26 + }, + { + "Dataset": "II", + "X": 5, + "Y": 4.74 + }, + { + "Dataset": "III", + "X": 10, + "Y": 7.46 + }, + { + "Dataset": "III", + "X": 8, + "Y": 6.77 + }, + { + "Dataset": "III", + "X": 13, + "Y": 12.74 + }, + { + "Dataset": "III", + "X": 9, + "Y": 7.11 + }, + { + "Dataset": "III", + "X": 11, + "Y": 7.81 + }, + { + "Dataset": "III", + "X": 14, + "Y": 8.84 + }, + { + "Dataset": "III", + "X": 6, + "Y": 6.08 + }, + { + "Dataset": "III", + "X": 4, + "Y": 5.39 + }, + { + "Dataset": "III", + "X": 12, + "Y": 8.15 + }, + { + "Dataset": "III", + "X": 7, + "Y": 6.42 + }, + { + "Dataset": "III", + "X": 5, + "Y": 5.73 + }, + { + "Dataset": "IV", + "X": 8, + "Y": 6.58 + }, + { + "Dataset": "IV", + "X": 8, + "Y": 5.76 + }, + { + "Dataset": "IV", + "X": 8, + "Y": 7.71 + }, + { + "Dataset": "IV", + "X": 8, + "Y": 8.84 + }, + { + "Dataset": "IV", + "X": 8, + "Y": 8.47 + }, + { + "Dataset": "IV", + "X": 8, + "Y": 7.04 + }, + { + "Dataset": "IV", + "X": 8, + "Y": 5.25 + }, + { + "Dataset": "IV", + "X": 19, + "Y": 12.5 + }, + { + "Dataset": "IV", + "X": 8, + "Y": 5.56 + }, + { + "Dataset": "IV", + "X": 8, + "Y": 7.91 + }, + { + "Dataset": "IV", + "X": 8, + "Y": 6.89 + } + ] + }, + "title": { + "text": "Anscombe's Quartet — same stats, different shapes" + } +} diff --git a/site/src/playground/theme-lab-assets/auto-mpg.flint.json b/site/src/playground/theme-lab-assets/auto-mpg.flint.json new file mode 100644 index 00000000..05ee658e --- /dev/null +++ b/site/src/playground/theme-lab-assets/auto-mpg.flint.json @@ -0,0 +1,186 @@ +{ + "layer": [ + { + "mark": "circle", + "encoding": { + "x": { + "field": "Horsepower", + "type": "quantitative", + "scale": { + "zero": false + }, + "axis": { + "format": ",.12~g" + } + }, + "y": { + "field": "MPG", + "type": "quantitative", + "scale": { + "zero": false + }, + "axis": { + "format": ",.12~g" + } + }, + "color": {}, + "size": {} + } + }, + { + "mark": { + "type": "line", + "color": "red" + }, + "transform": [ + { + "regression": "MPG", + "on": "Horsepower" + } + ], + "encoding": { + "x": { + "field": "Horsepower", + "type": "quantitative", + "scale": { + "zero": false + }, + "axis": { + "format": ",.12~g" + } + }, + "y": { + "field": "MPG", + "type": "quantitative", + "scale": { + "zero": false + }, + "axis": { + "format": ",.12~g" + } + } + } + } + ], + "encoding": {}, + "config": { + "view": { + "continuousWidth": 313, + "continuousHeight": 250 + }, + "axisX": { + "labelLimit": 100, + "labelFontSize": 10, + "titleFontSize": 11 + }, + "axisY": { + "labelFontSize": 10, + "titleFontSize": 11 + }, + "legend": { + "labelFontSize": 10, + "titleFontSize": 11 + }, + "facet": { + "spacing": 25 + } + }, + "data": { + "values": [ + { + "Horsepower": 130, + "MPG": 18 + }, + { + "Horsepower": 165, + "MPG": 15 + }, + { + "Horsepower": 150, + "MPG": 18 + }, + { + "Horsepower": 150, + "MPG": 16 + }, + { + "Horsepower": 140, + "MPG": 17 + }, + { + "Horsepower": 198, + "MPG": 15 + }, + { + "Horsepower": 220, + "MPG": 14 + }, + { + "Horsepower": 215, + "MPG": 14 + }, + { + "Horsepower": 97, + "MPG": 22 + }, + { + "Horsepower": 85, + "MPG": 26 + }, + { + "Horsepower": 88, + "MPG": 25 + }, + { + "Horsepower": 46, + "MPG": 26 + }, + { + "Horsepower": 90, + "MPG": 25 + }, + { + "Horsepower": 95, + "MPG": 24 + }, + { + "Horsepower": 68, + "MPG": 29 + }, + { + "Horsepower": 70, + "MPG": 27 + }, + { + "Horsepower": 52, + "MPG": 30 + }, + { + "Horsepower": 65, + "MPG": 31 + }, + { + "Horsepower": 67, + "MPG": 30 + }, + { + "Horsepower": 48, + "MPG": 43 + }, + { + "Horsepower": 66, + "MPG": 32 + }, + { + "Horsepower": 100, + "MPG": 22 + } + ] + }, + "title": { + "text": "Power against fuel economy", + "subtitle": [ + "Ordinary least squares fit; each point is one car model" + ] + } +} diff --git a/site/src/playground/theme-lab-assets/auto-mpg.nature.json b/site/src/playground/theme-lab-assets/auto-mpg.nature.json new file mode 100644 index 00000000..bb5ab5f5 --- /dev/null +++ b/site/src/playground/theme-lab-assets/auto-mpg.nature.json @@ -0,0 +1,136 @@ +{ + "$schema": "https://vega-lite.github.io/schema/vega-lite/v5.json", + "__theme__": "nature", + "__design__": [ + "Empty colour and size channels removed. Flint emits 'color: {}' and 'size: {}' on the point layer with no field behind them, which is a spec-level no-op that still costs Vega-Lite a scale resolution — and it is the reason this baseline throws a warning when compiled.", + "Fit statistics printed on the panel: slope, R² and n, computed by the same regression transform that draws the line. A trend line without a fit statistic is a drawing, not a result — a reviewer cannot tell a real relationship from a line through noise.", + "Regression line recoloured from Flint's literal 'red' to black, and the points to Okabe–Ito blue. Red is the convention for an error or an alert; the fit is neither, and in greyscale it becomes indistinguishable from the data anyway.", + "Line drawn *under* the points rather than over them, and clipped to the observed power range rather than run out to the panel edges — the fit should not occlude the observations it summarises, nor imply predictions outside the data it was estimated from.", + "Both axis titles given their units (hp, miles per US gallon) and the axis extents pinned, so the same panel can be reprinted beside another without the scales silently changing with the sample.", + "Black L-shaped spines with outward 3.5pt ticks, no gridlines, 8.5pt type and 89 mm column geometry." + ], + "background": "#ffffff", + "padding": { "left": 4, "top": 4, "right": 6, "bottom": 4 }, + "width": 220, + "height": 165, + "title": { + "text": "Power against fuel economy", + "subtitle": ["Ordinary least squares fit; each point is one car model"] + }, + "data": { + "values": [ + { "Horsepower": 130, "MPG": 18 }, + { "Horsepower": 165, "MPG": 15 }, + { "Horsepower": 150, "MPG": 18 }, + { "Horsepower": 150, "MPG": 16 }, + { "Horsepower": 140, "MPG": 17 }, + { "Horsepower": 198, "MPG": 15 }, + { "Horsepower": 220, "MPG": 14 }, + { "Horsepower": 215, "MPG": 14 }, + { "Horsepower": 97, "MPG": 22 }, + { "Horsepower": 85, "MPG": 26 }, + { "Horsepower": 88, "MPG": 25 }, + { "Horsepower": 46, "MPG": 26 }, + { "Horsepower": 90, "MPG": 25 }, + { "Horsepower": 95, "MPG": 24 }, + { "Horsepower": 68, "MPG": 29 }, + { "Horsepower": 70, "MPG": 27 }, + { "Horsepower": 52, "MPG": 30 }, + { "Horsepower": 65, "MPG": 31 }, + { "Horsepower": 67, "MPG": 30 }, + { "Horsepower": 48, "MPG": 43 }, + { "Horsepower": 66, "MPG": 32 }, + { "Horsepower": 100, "MPG": 22 } + ] + }, + "encoding": { + "x": { + "field": "Horsepower", + "type": "quantitative", + "title": "Engine power (hp)", + "scale": { "zero": false, "nice": false, "domain": [30, 235] }, + "axis": { "tickCount": 5, "format": "d" } + }, + "y": { + "field": "MPG", + "type": "quantitative", + "title": "Fuel economy (miles per US gallon)", + "scale": { "zero": false, "nice": false, "domain": [10, 46] }, + "axis": { "tickCount": 5, "format": "d" } + } + }, + "layer": [ + { + "transform": [{ "regression": "MPG", "on": "Horsepower", "extent": [46, 220] }], + "mark": { "type": "line", "color": "#000000", "strokeWidth": 1.1 } + }, + { + "mark": { + "type": "point", + "filled": true, + "size": 26, + "color": "#0072b2", + "stroke": "#ffffff", + "strokeWidth": 0.5 + } + }, + { + "transform": [ + { "regression": "MPG", "on": "Horsepower", "params": true }, + { + "calculate": "'slope = ' + format(datum.coef[1], '.3f') + ' mpg/hp · R² = ' + format(datum.rSquared, '.2f') + ' · n = 22'", + "as": "__fit" + }, + { "calculate": "233", "as": "Horsepower" }, + { "calculate": "45.5", "as": "MPG" } + ], + "mark": { + "type": "text", + "align": "right", + "baseline": "top", + "fontSize": 8, + "color": "#000000" + }, + "encoding": { + "text": { "field": "__fit", "type": "nominal" } + } + } + ], + "config": { + "background": "#ffffff", + "font": "Arial, Helvetica, sans-serif", + "title": { + "font": "Arial, Helvetica, sans-serif", + "fontSize": 12, + "fontWeight": 700, + "color": "#000000", + "anchor": "start", + "offset": 6, + "subtitleFont": "Arial, Helvetica, sans-serif", + "subtitleFontSize": 8, + "subtitleFontStyle": "italic", + "subtitleColor": "#3c3c3c", + "subtitlePadding": 5 + }, + "view": { "stroke": null }, + "axis": { + "labelFont": "Arial, Helvetica, sans-serif", + "labelFontSize": 8.5, + "labelColor": "#000000", + "labelPadding": 2, + "titleFont": "Arial, Helvetica, sans-serif", + "titleFontSize": 9, + "titleFontWeight": 400, + "titleColor": "#000000", + "titlePadding": 4, + "grid": false, + "domain": true, + "domainColor": "#000000", + "domainWidth": 1, + "ticks": true, + "tickColor": "#000000", + "tickWidth": 1, + "tickSize": 3.5 + } + } +} diff --git a/site/src/playground/theme-lab-assets/big-mac.economist.json b/site/src/playground/theme-lab-assets/big-mac.economist.json new file mode 100644 index 00000000..ec4c38de --- /dev/null +++ b/site/src/playground/theme-lab-assets/big-mac.economist.json @@ -0,0 +1,116 @@ +{ + "$schema": "https://vega-lite.github.io/schema/vega-lite/v5.json", + "__theme__": "economist", + "__design__": [ + "Masthead furniture added above the plot with a vconcat: a 26×3 red rect (#E3120B) that is pure brand identity, carrying no data. Vega-Lite has no header slot, so it has to be its own concat row.", + "Category order pinned explicitly rather than left as data order (Flint emits sort: null), so the ranking is a stated property of the chart instead of an accident of row order.", + "Value axis moved to the top (axis.orient) with gridlines only, and its title deleted — the deck already says the unit. The category axis loses its domain, ticks and title entirely.", + "House palette applied as a fixed colour, not a scheme: Economist blue #006BA2 for every bar, with red #E3120B held in reserve as the highlight hue.", + "Bar height narrowed to 0.68 of the band — the house style runs thinner bars with more gutter than Flint's default.", + "Type tightened to 9.5pt labels / 13.5pt bold headline with a grey deck: the same chart has to survive a two-column print grid." + ], + "background": "#ffffff", + "padding": { "left": 6, "top": 4, "right": 12, "bottom": 6 }, + "title": { + "text": "The price of a Big Mac", + "subtitle": ["2023, converted to US dollars at market exchange rates"] + }, + "spacing": 8, + "vconcat": [ + { + "data": { "values": [{}] }, + "mark": { "type": "rect", "fill": "#e3120b", "stroke": null }, + "width": 26, + "height": 3, + "view": { "stroke": null } + }, + { + "width": 280, + "height": 210, + "data": { + "values": [ + { "Country": "Switzerland", "Price (USD)": 8.1 }, + { "Country": "Norway", "Price (USD)": 6.9 }, + { "Country": "United States", "Price (USD)": 5.7 }, + { "Country": "Euro area", "Price (USD)": 5.5 }, + { "Country": "UK", "Price (USD)": 4.9 }, + { "Country": "Brazil", "Price (USD)": 4.5 }, + { "Country": "Mexico", "Price (USD)": 3.9 }, + { "Country": "China", "Price (USD)": 3.5 }, + { "Country": "Japan", "Price (USD)": 3.2 }, + { "Country": "Egypt", "Price (USD)": 2.7 }, + { "Country": "South Africa", "Price (USD)": 2.6 }, + { "Country": "India", "Price (USD)": 2.5 } + ] + }, + "encoding": { + "y": { + "field": "Country", + "type": "nominal", + "sort": [ + "Switzerland", + "Norway", + "United States", + "Euro area", + "UK", + "Brazil", + "Mexico", + "China", + "Japan", + "Egypt", + "South Africa", + "India" + ], + "title": null, + "axis": { "domain": false, "ticks": false, "labelPadding": 4 } + }, + "x": { + "field": "Price (USD)", + "type": "quantitative", + "title": null, + "scale": { "zero": true }, + "axis": { + "orient": "top", + "grid": true, + "gridColor": "#c9d3da", + "domain": false, + "ticks": false, + "tickCount": 5, + "format": "$.0f", + "labelPadding": 2 + } + } + }, + "mark": { "type": "bar", "height": { "band": 0.68 }, "color": "#006ba2" } + } + ], + "config": { + "background": "#ffffff", + "font": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "title": { + "font": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "fontSize": 13.5, + "fontWeight": 700, + "color": "#121317", + "anchor": "start", + "offset": 10, + "subtitleFont": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "subtitleFontSize": 10.5, + "subtitleColor": "#54585a", + "subtitlePadding": 6 + }, + "view": { "stroke": null }, + "axis": { + "labelFont": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "labelFontSize": 9.5, + "labelColor": "#54585a", + "titleFont": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "titleFontSize": 9.5, + "titleFontWeight": 400, + "titleColor": "#54585a", + "grid": false, + "domain": false, + "ticks": false + } + } +} diff --git a/site/src/playground/theme-lab-assets/big-mac.flint.json b/site/src/playground/theme-lab-assets/big-mac.flint.json new file mode 100644 index 00000000..6344614f --- /dev/null +++ b/site/src/playground/theme-lab-assets/big-mac.flint.json @@ -0,0 +1,103 @@ +{ + "mark": "bar", + "encoding": { + "x": { + "field": "Price (USD)", + "type": "quantitative", + "scale": { + "zero": true + }, + "axis": { + "format": ",.12~g" + } + }, + "y": { + "field": "Country", + "type": "nominal", + "sort": null + } + }, + "config": { + "view": { + "continuousWidth": 261, + "continuousHeight": 230 + }, + "axisX": { + "labelLimit": 100, + "labelFontSize": 10, + "titleFontSize": 11 + }, + "axisY": { + "labelFontSize": 10, + "titleFontSize": 11 + }, + "legend": { + "labelFontSize": 10, + "titleFontSize": 11 + }, + "facet": { + "spacing": 23 + } + }, + "height": { + "step": 20 + }, + "data": { + "values": [ + { + "Country": "Switzerland", + "Price (USD)": 8.1 + }, + { + "Country": "Norway", + "Price (USD)": 6.9 + }, + { + "Country": "United States", + "Price (USD)": 5.7 + }, + { + "Country": "Euro area", + "Price (USD)": 5.5 + }, + { + "Country": "UK", + "Price (USD)": 4.9 + }, + { + "Country": "Brazil", + "Price (USD)": 4.5 + }, + { + "Country": "Mexico", + "Price (USD)": 3.9 + }, + { + "Country": "China", + "Price (USD)": 3.5 + }, + { + "Country": "Japan", + "Price (USD)": 3.2 + }, + { + "Country": "Egypt", + "Price (USD)": 2.7 + }, + { + "Country": "South Africa", + "Price (USD)": 2.6 + }, + { + "Country": "India", + "Price (USD)": 2.5 + } + ] + }, + "title": { + "text": "The price of a Big Mac", + "subtitle": [ + "2023, converted to US dollars at market exchange rates" + ] + } +} diff --git a/site/src/playground/theme-lab-assets/browser-pie.datawrapper.json b/site/src/playground/theme-lab-assets/browser-pie.datawrapper.json new file mode 100644 index 00000000..8cce52f5 --- /dev/null +++ b/site/src/playground/theme-lab-assets/browser-pie.datawrapper.json @@ -0,0 +1,172 @@ +{ + "$schema": "https://vega-lite.github.io/schema/vega-lite/v5.json", + "__theme__": "datawrapper", + "__design__": [ + "Still a pie. The form is weak here — Safari and Edge are both on 12 per cent, and as two arcs of equal angle at different orientations no reader can confirm that by looking — but the question this table asks is what a design language does to a chart, not what it replaces it with. So the tie is made checkable by printing the numbers, not by abandoning the geometry.", + "Values printed outside every slice. Flint's pie states no quantities at all: the reader gets five arcs and a colour key and is asked to estimate. With 65, 12, 12, 6 and 5 on the page the two 12s are identical characters and the tie is settled.", + "Labels set outside the wedges in near-black, not inside them in white. None of the five house inks clears 4.5:1 against white type — the orange is nearer 2:1 — so an accessibility-first language cannot use the usual place for the number. Outside, on white, every label runs at about 12:1.", + "tableau10 replaced by the five house inks as an explicit domain/range pair, so a browser keeps its colour across every chart in a story rather than taking whatever hue its row index happens to draw.", + "Legend kept, but moved to the top as a horizontal strip of square swatches with no title. A pie is the one form that genuinely needs a key — the categories live in the marks and there is no axis to name them — so the key is restyled rather than removed.", + "Slice order left exactly as the baseline has it, running clockwise from twelve o'clock in source order. Sorting by share would read better — Chrome's 65 per cent would open the circle and the two twelves would end up adjacent — but which wedge follows which is a statement about the data, decided upstream of any house style.", + "A 1.5px white separator between slices — what keeps two adjacent hues legible as two shapes for a reader who cannot tell the hues apart.", + "11pt type floor, no frame, hairline footer." + ], + "background": "#ffffff", + "padding": { "left": 8, "top": 6, "right": 16, "bottom": 8 }, + "title": { + "text": "Chrome holds two-thirds of the desktop market", + "subtitle": ["Desktop browser share, 2024, per cent"] + }, + "spacing": 8, + "vconcat": [ + { + "width": 260, + "height": 190, + "data": { + "values": [ + { "Browser": "Chrome", "Share": 65 }, + { "Browser": "Safari", "Share": 12 }, + { "Browser": "Edge", "Share": 12 }, + { "Browser": "Firefox", "Share": 6 }, + { "Browser": "Other", "Share": 5 } + ] + }, + "transform": [ + { + "window": [{ "op": "row_number", "as": "__i" }], + "frame": [null, null] + }, + { + "window": [{ "op": "sum", "field": "Share", "as": "__cum" }], + "sort": [{ "field": "__i", "order": "ascending" }], + "frame": [null, 0] + }, + { "calculate": "(datum.__cum - datum.Share) / 100 * 2 * PI", "as": "__start" }, + { "calculate": "datum.__cum / 100 * 2 * PI", "as": "__end" }, + { "calculate": "(datum.__cum - datum.Share / 2) / 100 * 2 * PI", "as": "__mid" }, + { "calculate": "datum.Share + '%'", "as": "__pct" } + ], + "encoding": { + "color": { + "field": "Browser", + "type": "nominal", + "title": null, + "scale": { + "domain": ["Chrome", "Safari", "Edge", "Firefox", "Other"], + "range": ["#18a1cd", "#e2a233", "#c04a4a", "#2d8659", "#7e5aa2"] + } + } + }, + "layer": [ + { + "mark": { + "type": "arc", + "outerRadius": 70, + "stroke": "#ffffff", + "strokeWidth": 1.5 + }, + "encoding": { + "theta": { + "field": "__start", + "type": "quantitative", + "scale": { "domain": [0, 6.283185307179586], "range": [0, 6.283185307179586] } + }, + "theta2": { "field": "__end" } + } + }, + { + "transform": [{ "filter": "datum.__mid < PI" }], + "mark": { + "type": "text", + "radius": 80, + "align": "left", + "baseline": "middle", + "fontSize": 11 + }, + "encoding": { + "theta": { + "field": "__mid", + "type": "quantitative", + "scale": { "domain": [0, 6.283185307179586], "range": [0, 6.283185307179586] } + }, + "text": { "field": "__pct", "type": "nominal" }, + "color": { "value": "#333333" } + } + }, + { + "transform": [{ "filter": "datum.__mid >= PI" }], + "mark": { + "type": "text", + "radius": 80, + "align": "right", + "baseline": "middle", + "fontSize": 11 + }, + "encoding": { + "theta": { + "field": "__mid", + "type": "quantitative", + "scale": { "domain": [0, 6.283185307179586], "range": [0, 6.283185307179586] } + }, + "text": { "field": "__pct", "type": "nominal" }, + "color": { "value": "#333333" } + } + } + ] + }, + { + "data": { "values": [{}] }, + "mark": { "type": "rect", "fill": "#dcdcdc", "stroke": null }, + "width": 270, + "height": 1, + "view": { "stroke": null } + } + ], + "config": { + "background": "#ffffff", + "font": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "title": { + "font": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "fontSize": 14.5, + "fontWeight": 700, + "color": "#333333", + "anchor": "start", + "offset": 12, + "lineHeight": 18, + "subtitleFont": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "subtitleFontSize": 11.5, + "subtitleColor": "#666666", + "subtitlePadding": 7, + "subtitleLineHeight": 15 + }, + "view": { "stroke": null }, + "legend": { + "orient": "top", + "direction": "horizontal", + "title": null, + "labelFont": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "labelFontSize": 11, + "labelColor": "#333333", + "symbolType": "square", + "symbolSize": 100, + "symbolStrokeWidth": 0, + "labelOffset": 5, + "columnPadding": 14, + "offset": 2, + "padding": 0 + }, + "axis": { + "labelFont": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "labelFontSize": 11, + "labelColor": "#333333", + "labelPadding": 5, + "titleFont": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "titleFontSize": 10.5, + "titleFontWeight": 400, + "titleColor": "#767676", + "grid": false, + "domain": false, + "ticks": false + } + } +} diff --git a/site/src/playground/theme-lab-assets/browser-pie.economist.json b/site/src/playground/theme-lab-assets/browser-pie.economist.json new file mode 100644 index 00000000..24fa766f --- /dev/null +++ b/site/src/playground/theme-lab-assets/browser-pie.economist.json @@ -0,0 +1,122 @@ +{ + "$schema": "https://vega-lite.github.io/schema/vega-lite/v5.json", + "__theme__": "economist", + "__design__": [ + "Legend deleted, names written against the wedges. Same answer as the NYT row, and the second time these two have converged on a chart where the categories live inside the marks — on the ranked bar they took opposite sides. A key is a lookup table, and neither house prints a lookup table when the label can go where the reader already is.", + "Shares printed with the names. Safari and Edge are both on 12, and two equal angles at different orientations cannot be compared by eye; the numbers are what make the tie checkable. On a pie no language can fall back on the axis, so the values-or-axis disagreement that separates these six elsewhere never arises.", + "House palette in place of tableau10, ordered so the two twelves are not adjacent hues of the same temperature, with Other in the sand tone the paper reserves for residuals.", + "Red masthead rule, sans headline, no frame — the fixtures that make the chart recognisable before it is read.", + "White separators at 1.5px between arcs.", + "Slice order left exactly as the baseline has it, clockwise from twelve o'clock in source order." + ], + "background": "#ffffff", + "padding": { "left": 8, "top": 6, "right": 10, "bottom": 8 }, + "title": { + "text": "Chrome holds two-thirds of the desktop market", + "subtitle": ["Desktop browser share, 2024, per cent"] + }, + "spacing": 6, + "vconcat": [ + { + "data": { "values": [{}] }, + "mark": { "type": "rect", "fill": "#e3120b", "stroke": null }, + "width": 26, + "height": 3, + "view": { "stroke": null } + }, + { + "width": 245, + "height": 195, + "data": { + "values": [ + { "Browser": "Chrome", "Share": 65 }, + { "Browser": "Safari", "Share": 12 }, + { "Browser": "Edge", "Share": 12 }, + { "Browser": "Firefox", "Share": 6 }, + { "Browser": "Other", "Share": 5 } + ] + }, + "transform": [ + { "window": [{ "op": "row_number", "as": "__i" }], "frame": [null, null] }, + { + "window": [{ "op": "sum", "field": "Share", "as": "__cum" }], + "sort": [{ "field": "__i", "order": "ascending" }], + "frame": [null, 0] + }, + { "calculate": "(datum.__cum - datum.Share) / 100 * 2 * PI", "as": "__start" }, + { "calculate": "datum.__cum / 100 * 2 * PI", "as": "__end" }, + { "calculate": "(datum.__cum - datum.Share / 2) / 100 * 2 * PI", "as": "__mid" }, + { "calculate": "datum.Browser + ' ' + datum.Share + '%'", "as": "__lab" } + ], + "encoding": { + "color": { + "field": "Browser", + "type": "nominal", + "legend": null, + "scale": { + "domain": ["Chrome", "Safari", "Edge", "Firefox", "Other"], + "range": ["#006ba2", "#3ebcd2", "#a1655a", "#3f5661", "#c8b88a"] + } + } + }, + "layer": [ + { + "mark": { "type": "arc", "outerRadius": 60, "stroke": "#ffffff", "strokeWidth": 1.5 }, + "encoding": { + "theta": { + "field": "__start", + "type": "quantitative", + "scale": { "domain": [0, 6.283185307179586], "range": [0, 6.283185307179586] } + }, + "theta2": { "field": "__end" } + } + }, + { + "transform": [{ "filter": "datum.__mid < PI" }], + "mark": { "type": "text", "radius": 68, "align": "left", "baseline": "middle", "fontSize": 10 }, + "encoding": { + "theta": { + "field": "__mid", + "type": "quantitative", + "scale": { "domain": [0, 6.283185307179586], "range": [0, 6.283185307179586] } + }, + "text": { "field": "__lab", "type": "nominal" }, + "color": { "value": "#1c2b36" } + } + }, + { + "transform": [{ "filter": "datum.__mid >= PI" }], + "mark": { "type": "text", "radius": 68, "align": "right", "baseline": "middle", "fontSize": 10 }, + "encoding": { + "theta": { + "field": "__mid", + "type": "quantitative", + "scale": { "domain": [0, 6.283185307179586], "range": [0, 6.283185307179586] } + }, + "text": { "field": "__lab", "type": "nominal" }, + "color": { "value": "#1c2b36" } + } + } + ] + } + ], + "config": { + "background": "#ffffff", + "font": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "title": { + "font": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "fontSize": 13.5, + "fontWeight": 700, + "color": "#1c2b36", + "anchor": "start", + "offset": 8, + "lineHeight": 17, + "subtitleFont": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "subtitleFontSize": 10.5, + "subtitleColor": "#63737f", + "subtitlePadding": 6 + }, + "view": { "stroke": null }, + "text": { "font": "'Helvetica Neue', Helvetica, Arial, sans-serif" } + } +} diff --git a/site/src/playground/theme-lab-assets/browser-pie.flint.json b/site/src/playground/theme-lab-assets/browser-pie.flint.json new file mode 100644 index 00000000..0456ee92 --- /dev/null +++ b/site/src/playground/theme-lab-assets/browser-pie.flint.json @@ -0,0 +1,71 @@ +{ + "mark": "arc", + "encoding": { + "theta": { + "field": "Share", + "type": "quantitative" + }, + "color": { + "field": "Browser", + "type": "nominal", + "sort": null, + "scale": { + "scheme": "tableau10" + } + } + }, + "width": 340, + "height": 292, + "config": { + "view": { + "continuousWidth": 340, + "continuousHeight": 292 + }, + "axisX": { + "labelLimit": 100, + "labelFontSize": 10, + "titleFontSize": 11 + }, + "axisY": { + "labelFontSize": 10, + "titleFontSize": 11 + }, + "legend": { + "labelFontSize": 10, + "titleFontSize": 11 + }, + "facet": { + "spacing": 23 + } + }, + "data": { + "values": [ + { + "Browser": "Chrome", + "Share": 65 + }, + { + "Browser": "Safari", + "Share": 12 + }, + { + "Browser": "Edge", + "Share": 12 + }, + { + "Browser": "Firefox", + "Share": 6 + }, + { + "Browser": "Other", + "Share": 5 + } + ] + }, + "title": { + "text": "Chrome holds two-thirds of the desktop market", + "subtitle": [ + "Desktop browser share, 2024, per cent" + ] + } +} diff --git a/site/src/playground/theme-lab-assets/browser-pie.mckinsey.json b/site/src/playground/theme-lab-assets/browser-pie.mckinsey.json new file mode 100644 index 00000000..9d91f916 --- /dev/null +++ b/site/src/playground/theme-lab-assets/browser-pie.mckinsey.json @@ -0,0 +1,109 @@ +{ + "$schema": "https://vega-lite.github.io/schema/vega-lite/v5.json", + "__theme__": "mckinsey", + "__design__": [ + "Five categorical hues replaced by one hue at five lightness steps. Flint's tableau10 gives Chrome, Safari and Edge unrelated colours, which reads as five unrelated things; they are five parts of one quantity, and a single-hue ramp says that without any annotation. The ramp follows the slice order, which here runs from the largest share to the smallest, so it restates what the arcs already show rather than asserting anything extra.", + "Legend deleted, name and share written against each wedge. The key is a lookup step, and the number is the deliverable — the same reading this language applied to the ranked bar and to the line.", + "Values printed because angle cannot be read. Safari and Edge are both 12 per cent; as two equal arcs at different orientations the tie is invisible, and there is no axis to fall back on.", + "Labels in navy on white, outside the circle. Inside would put type over a ramp that runs from near-black to pale, so the same label colour cannot clear contrast on both ends.", + "No frame, no fill, nothing behind the circle. 1px white separators are the only scaffolding left.", + "Slice order left exactly as the baseline has it, clockwise from twelve o'clock in source order." + ], + "background": "#ffffff", + "padding": { "left": 8, "top": 6, "right": 10, "bottom": 8 }, + "title": { + "text": "Chrome holds two-thirds of the desktop market", + "subtitle": ["Desktop browser share, 2024, per cent"] + }, + "width": 250, + "height": 205, + "data": { + "values": [ + { "Browser": "Chrome", "Share": 65 }, + { "Browser": "Safari", "Share": 12 }, + { "Browser": "Edge", "Share": 12 }, + { "Browser": "Firefox", "Share": 6 }, + { "Browser": "Other", "Share": 5 } + ] + }, + "transform": [ + { "window": [{ "op": "row_number", "as": "__i" }], "frame": [null, null] }, + { + "window": [{ "op": "sum", "field": "Share", "as": "__cum" }], + "sort": [{ "field": "__i", "order": "ascending" }], + "frame": [null, 0] + }, + { "calculate": "(datum.__cum - datum.Share) / 100 * 2 * PI", "as": "__start" }, + { "calculate": "datum.__cum / 100 * 2 * PI", "as": "__end" }, + { "calculate": "(datum.__cum - datum.Share / 2) / 100 * 2 * PI", "as": "__mid" }, + { "calculate": "datum.Browser + ' ' + datum.Share + '%'", "as": "__lab" } + ], + "encoding": { + "color": { + "field": "Browser", + "type": "nominal", + "legend": null, + "scale": { + "domain": ["Chrome", "Safari", "Edge", "Firefox", "Other"], + "range": ["#051c2c", "#12405c", "#2a6f95", "#6ba4c4", "#b8d3e2"] + } + } + }, + "layer": [ + { + "mark": { "type": "arc", "outerRadius": 62, "stroke": "#ffffff", "strokeWidth": 1 }, + "encoding": { + "theta": { + "field": "__start", + "type": "quantitative", + "scale": { "domain": [0, 6.283185307179586], "range": [0, 6.283185307179586] } + }, + "theta2": { "field": "__end" } + } + }, + { + "transform": [{ "filter": "datum.__mid < PI" }], + "mark": { "type": "text", "radius": 70, "align": "left", "baseline": "middle", "fontSize": 10 }, + "encoding": { + "theta": { + "field": "__mid", + "type": "quantitative", + "scale": { "domain": [0, 6.283185307179586], "range": [0, 6.283185307179586] } + }, + "text": { "field": "__lab", "type": "nominal" }, + "color": { "value": "#051c2c" } + } + }, + { + "transform": [{ "filter": "datum.__mid >= PI" }], + "mark": { "type": "text", "radius": 70, "align": "right", "baseline": "middle", "fontSize": 10 }, + "encoding": { + "theta": { + "field": "__mid", + "type": "quantitative", + "scale": { "domain": [0, 6.283185307179586], "range": [0, 6.283185307179586] } + }, + "text": { "field": "__lab", "type": "nominal" }, + "color": { "value": "#051c2c" } + } + } + ], + "config": { + "background": "#ffffff", + "font": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "title": { + "font": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "fontSize": 14, + "fontWeight": 700, + "color": "#051c2c", + "anchor": "start", + "offset": 12, + "subtitleFont": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "subtitleFontSize": 10.5, + "subtitleColor": "#5c6b75", + "subtitlePadding": 6 + }, + "view": { "stroke": null }, + "text": { "font": "'Helvetica Neue', Helvetica, Arial, sans-serif" } + } +} diff --git a/site/src/playground/theme-lab-assets/browser-pie.nature.json b/site/src/playground/theme-lab-assets/browser-pie.nature.json new file mode 100644 index 00000000..88c53ace --- /dev/null +++ b/site/src/playground/theme-lab-assets/browser-pie.nature.json @@ -0,0 +1,120 @@ +{ + "$schema": "https://vega-lite.github.io/schema/vega-lite/v5.json", + "__theme__": "nature", + "__design__": [ + "The key is kept and the shares are printed as well — the category named twice, once in the legend and once against its own wedge. Every other language here treats that as waste and picks one. A figure that will be reproduced at column width, photocopied and cited on its own cannot assume the reader has the caption, so this is the third cluster in a row where this language answers a problem by adding rather than removing.", + "Palette is the Okabe-Ito colour-vision-safe set with grey for Other. Flint's tableau10 puts a green and an orange of near-equal lightness on the two 12 per cent slices; under deuteranopia those are one hue, and the two arcs are the same size, so the pair becomes genuinely unreadable rather than merely hard.", + "1.5px white separators, which is what keeps two arcs legible as two shapes once hue is unreliable.", + "Values are unavoidable here. Safari and Edge are both 12 per cent, and equal angles at different orientations cannot be compared; this is the one form where no language gets to trust the geometry.", + "Type at 8.5pt throughout so the key fits beside a 190-point plot without the figure growing.", + "Slice order left exactly as the baseline has it, clockwise from twelve o'clock in source order." + ], + "background": "#ffffff", + "padding": { "left": 8, "top": 6, "right": 10, "bottom": 6 }, + "title": { + "text": "Chrome holds two-thirds of the desktop market", + "subtitle": ["Desktop browser share, 2024, per cent"] + }, + "width": 190, + "height": 165, + "data": { + "values": [ + { "Browser": "Chrome", "Share": 65 }, + { "Browser": "Safari", "Share": 12 }, + { "Browser": "Edge", "Share": 12 }, + { "Browser": "Firefox", "Share": 6 }, + { "Browser": "Other", "Share": 5 } + ] + }, + "transform": [ + { "window": [{ "op": "row_number", "as": "__i" }], "frame": [null, null] }, + { + "window": [{ "op": "sum", "field": "Share", "as": "__cum" }], + "sort": [{ "field": "__i", "order": "ascending" }], + "frame": [null, 0] + }, + { "calculate": "(datum.__cum - datum.Share) / 100 * 2 * PI", "as": "__start" }, + { "calculate": "datum.__cum / 100 * 2 * PI", "as": "__end" }, + { "calculate": "(datum.__cum - datum.Share / 2) / 100 * 2 * PI", "as": "__mid" }, + { "calculate": "datum.Share + '%'", "as": "__pct" } + ], + "encoding": { + "color": { + "field": "Browser", + "type": "nominal", + "title": null, + "scale": { + "domain": ["Chrome", "Safari", "Edge", "Firefox", "Other"], + "range": ["#0072b2", "#e69f00", "#009e73", "#cc79a7", "#999999"] + }, + "legend": { + "orient": "right", + "labelFontSize": 8.5, + "labelColor": "#000000", + "symbolType": "square", + "symbolSize": 60, + "symbolStrokeWidth": 0, + "offset": 4 + } + } + }, + "layer": [ + { + "mark": { "type": "arc", "outerRadius": 58, "stroke": "#ffffff", "strokeWidth": 1.5 }, + "encoding": { + "theta": { + "field": "__start", + "type": "quantitative", + "scale": { "domain": [0, 6.283185307179586], "range": [0, 6.283185307179586] } + }, + "theta2": { "field": "__end" } + } + }, + { + "transform": [{ "filter": "datum.__mid < PI" }], + "mark": { "type": "text", "radius": 66, "align": "left", "baseline": "middle", "fontSize": 8.5 }, + "encoding": { + "theta": { + "field": "__mid", + "type": "quantitative", + "scale": { "domain": [0, 6.283185307179586], "range": [0, 6.283185307179586] } + }, + "text": { "field": "__pct", "type": "nominal" }, + "color": { "value": "#000000" } + } + }, + { + "transform": [{ "filter": "datum.__mid >= PI" }], + "mark": { "type": "text", "radius": 66, "align": "right", "baseline": "middle", "fontSize": 8.5 }, + "encoding": { + "theta": { + "field": "__mid", + "type": "quantitative", + "scale": { "domain": [0, 6.283185307179586], "range": [0, 6.283185307179586] } + }, + "text": { "field": "__pct", "type": "nominal" }, + "color": { "value": "#000000" } + } + } + ], + "config": { + "background": "#ffffff", + "font": "Arial, Helvetica, sans-serif", + "title": { + "font": "Arial, Helvetica, sans-serif", + "fontSize": 12, + "fontWeight": 700, + "color": "#000000", + "anchor": "start", + "offset": 6, + "subtitleFont": "Arial, Helvetica, sans-serif", + "subtitleFontSize": 8, + "subtitleFontStyle": "italic", + "subtitleColor": "#3c3c3c", + "subtitlePadding": 5 + }, + "view": { "stroke": null }, + "legend": { "labelFont": "Arial, Helvetica, sans-serif" }, + "text": { "font": "Arial, Helvetica, sans-serif" } + } +} diff --git a/site/src/playground/theme-lab-assets/browser-pie.nyt.json b/site/src/playground/theme-lab-assets/browser-pie.nyt.json new file mode 100644 index 00000000..6617da1c --- /dev/null +++ b/site/src/playground/theme-lab-assets/browser-pie.nyt.json @@ -0,0 +1,110 @@ +{ + "$schema": "https://vega-lite.github.io/schema/vega-lite/v5.json", + "__theme__": "nyt", + "__design__": [ + "Legend deleted, every slice named on the page next to its own wedge. Flint's key sits to the right in source order, so reading the chart means matching five hues to five words and carrying the match back into the circle. With the name written against the arc there is nothing to match.", + "The share is printed with the name. This is not a stylistic addition: Safari and Edge are both on 12 per cent, and as two arcs of equal angle at different orientations that tie is unconfirmable by eye. Angle is the one channel where a language cannot decide to trust the geometry — which is why the values-or-axis split that separates these six on a bar chart has nothing to bite on here.", + "Palette reduced to four working hues plus grey for Other. Flint's tableau10 gives Other a saturated purple, the same weight as Chrome, which makes a residual category look like a finding.", + "White separators at 1.5px so two adjacent arcs stay two shapes.", + "Serif headline, small grey deck, no frame, no fill behind the circle.", + "Slice order left exactly as the baseline has it, clockwise from twelve o'clock in source order." + ], + "background": "#ffffff", + "padding": { "left": 8, "top": 6, "right": 10, "bottom": 8 }, + "title": { + "text": "Chrome holds two-thirds of the desktop market", + "subtitle": ["Desktop browser share, 2024, per cent"] + }, + "width": 250, + "height": 205, + "data": { + "values": [ + { "Browser": "Chrome", "Share": 65 }, + { "Browser": "Safari", "Share": 12 }, + { "Browser": "Edge", "Share": 12 }, + { "Browser": "Firefox", "Share": 6 }, + { "Browser": "Other", "Share": 5 } + ] + }, + "transform": [ + { "window": [{ "op": "row_number", "as": "__i" }], "frame": [null, null] }, + { + "window": [{ "op": "sum", "field": "Share", "as": "__cum" }], + "sort": [{ "field": "__i", "order": "ascending" }], + "frame": [null, 0] + }, + { "calculate": "(datum.__cum - datum.Share) / 100 * 2 * PI", "as": "__start" }, + { "calculate": "datum.__cum / 100 * 2 * PI", "as": "__end" }, + { "calculate": "(datum.__cum - datum.Share / 2) / 100 * 2 * PI", "as": "__mid" }, + { "calculate": "datum.Browser + ' ' + datum.Share + '%'", "as": "__lab" } + ], + "encoding": { + "color": { + "field": "Browser", + "type": "nominal", + "legend": null, + "scale": { + "domain": ["Chrome", "Safari", "Edge", "Firefox", "Other"], + "range": ["#2f6b9a", "#c2352b", "#5c8a3c", "#9a6c3f", "#9e9e9e"] + } + } + }, + "layer": [ + { + "mark": { "type": "arc", "outerRadius": 62, "stroke": "#ffffff", "strokeWidth": 1.5 }, + "encoding": { + "theta": { + "field": "__start", + "type": "quantitative", + "scale": { "domain": [0, 6.283185307179586], "range": [0, 6.283185307179586] } + }, + "theta2": { "field": "__end" } + } + }, + { + "transform": [{ "filter": "datum.__mid < PI" }], + "mark": { "type": "text", "radius": 70, "align": "left", "baseline": "middle", "fontSize": 10.5 }, + "encoding": { + "theta": { + "field": "__mid", + "type": "quantitative", + "scale": { "domain": [0, 6.283185307179586], "range": [0, 6.283185307179586] } + }, + "text": { "field": "__lab", "type": "nominal" }, + "color": { "value": "#121212" } + } + }, + { + "transform": [{ "filter": "datum.__mid >= PI" }], + "mark": { "type": "text", "radius": 70, "align": "right", "baseline": "middle", "fontSize": 10.5 }, + "encoding": { + "theta": { + "field": "__mid", + "type": "quantitative", + "scale": { "domain": [0, 6.283185307179586], "range": [0, 6.283185307179586] } + }, + "text": { "field": "__lab", "type": "nominal" }, + "color": { "value": "#121212" } + } + } + ], + "config": { + "background": "#ffffff", + "font": "Helvetica, Arial, sans-serif", + "title": { + "font": "Georgia, 'Times New Roman', serif", + "fontSize": 15, + "fontWeight": 700, + "color": "#121212", + "anchor": "start", + "offset": 10, + "lineHeight": 19, + "subtitleFont": "Helvetica, Arial, sans-serif", + "subtitleFontSize": 11, + "subtitleColor": "#6b6b6b", + "subtitlePadding": 6 + }, + "view": { "stroke": null }, + "text": { "font": "Helvetica, Arial, sans-serif" } + } +} diff --git a/site/src/playground/theme-lab-assets/browser-pie.powerbi.json b/site/src/playground/theme-lab-assets/browser-pie.powerbi.json new file mode 100644 index 00000000..3d7d7be8 --- /dev/null +++ b/site/src/playground/theme-lab-assets/browser-pie.powerbi.json @@ -0,0 +1,119 @@ +{ + "$schema": "https://vega-lite.github.io/schema/vega-lite/v5.json", + "__theme__": "powerbi", + "__design__": [ + "The key is kept, on the right. A tile is a query result: the category set changes when a slicer moves, and a legend redraws in place where labels pinned to particular wedges have to be re-laid out. The three languages that direct-label here are authoring a fixed picture; this one is describing something that will be filtered.", + "Shares printed against the arcs anyway. Safari and Edge are both on 12 per cent and equal angles at different orientations cannot be compared — the one form where no language gets to trust the geometry, since there is no axis to fall back on.", + "Palette lifted to survive the dark ground. Flint's tableau10 is mixed for white; at this brightness its mid-tones go muddy and the two 12 per cent slices stop separating.", + "Separators drawn in the ground colour rather than white, so the gaps read as the panel showing through instead of as a fifth ring of ink.", + "Labels at 9.5pt in #C8C6C4 rather than white: pure white on near-black blooms, and a light grey holds contrast without the halo.", + "Slice order left exactly as the baseline has it, clockwise from twelve o'clock in source order." + ], + "background": "#1b1a19", + "padding": { "left": 10, "top": 8, "right": 12, "bottom": 8 }, + "title": { + "text": "Chrome holds two-thirds of the desktop market", + "subtitle": ["Desktop browser share, 2024, per cent"] + }, + "width": 215, + "height": 185, + "data": { + "values": [ + { "Browser": "Chrome", "Share": 65 }, + { "Browser": "Safari", "Share": 12 }, + { "Browser": "Edge", "Share": 12 }, + { "Browser": "Firefox", "Share": 6 }, + { "Browser": "Other", "Share": 5 } + ] + }, + "transform": [ + { "window": [{ "op": "row_number", "as": "__i" }], "frame": [null, null] }, + { + "window": [{ "op": "sum", "field": "Share", "as": "__cum" }], + "sort": [{ "field": "__i", "order": "ascending" }], + "frame": [null, 0] + }, + { "calculate": "(datum.__cum - datum.Share) / 100 * 2 * PI", "as": "__start" }, + { "calculate": "datum.__cum / 100 * 2 * PI", "as": "__end" }, + { "calculate": "(datum.__cum - datum.Share / 2) / 100 * 2 * PI", "as": "__mid" }, + { "calculate": "datum.Share + '%'", "as": "__pct" } + ], + "encoding": { + "color": { + "field": "Browser", + "type": "nominal", + "title": null, + "scale": { + "domain": ["Chrome", "Safari", "Edge", "Firefox", "Other"], + "range": ["#118dff", "#e66c37", "#3bd1c7", "#e044a7", "#8a8886"] + }, + "legend": { + "orient": "right", + "labelFontSize": 9, + "labelColor": "#c8c6c4", + "symbolType": "square", + "symbolSize": 70, + "symbolStrokeWidth": 0, + "offset": 6 + } + } + }, + "layer": [ + { + "mark": { "type": "arc", "outerRadius": 62, "stroke": "#1b1a19", "strokeWidth": 1.5 }, + "encoding": { + "theta": { + "field": "__start", + "type": "quantitative", + "scale": { "domain": [0, 6.283185307179586], "range": [0, 6.283185307179586] } + }, + "theta2": { "field": "__end" } + } + }, + { + "transform": [{ "filter": "datum.__mid < PI" }], + "mark": { "type": "text", "radius": 70, "align": "left", "baseline": "middle", "fontSize": 9.5 }, + "encoding": { + "theta": { + "field": "__mid", + "type": "quantitative", + "scale": { "domain": [0, 6.283185307179586], "range": [0, 6.283185307179586] } + }, + "text": { "field": "__pct", "type": "nominal" }, + "color": { "value": "#c8c6c4" } + } + }, + { + "transform": [{ "filter": "datum.__mid >= PI" }], + "mark": { "type": "text", "radius": 70, "align": "right", "baseline": "middle", "fontSize": 9.5 }, + "encoding": { + "theta": { + "field": "__mid", + "type": "quantitative", + "scale": { "domain": [0, 6.283185307179586], "range": [0, 6.283185307179586] } + }, + "text": { "field": "__pct", "type": "nominal" }, + "color": { "value": "#c8c6c4" } + } + } + ], + "config": { + "background": "#1b1a19", + "font": "'Segoe UI', 'Segoe UI Variable', system-ui, -apple-system, sans-serif", + "title": { + "font": "'Segoe UI', 'Segoe UI Variable', system-ui, sans-serif", + "fontSize": 12.5, + "fontWeight": 600, + "color": "#f3f2f1", + "anchor": "start", + "offset": 10, + "subtitleFont": "'Segoe UI', 'Segoe UI Variable', system-ui, sans-serif", + "subtitleFontSize": 10, + "subtitleColor": "#a19f9d", + "subtitlePadding": 5 + }, + "view": { "stroke": null }, + "legend": { "labelFont": "'Segoe UI', 'Segoe UI Variable', system-ui, sans-serif" }, + "text": { "font": "'Segoe UI', 'Segoe UI Variable', system-ui, sans-serif" } + } +} diff --git a/site/src/playground/theme-lab-assets/causes-death.datawrapper.json b/site/src/playground/theme-lab-assets/causes-death.datawrapper.json new file mode 100644 index 00000000..35089f10 --- /dev/null +++ b/site/src/playground/theme-lab-assets/causes-death.datawrapper.json @@ -0,0 +1,136 @@ +{ + "$schema": "https://vega-lite.github.io/schema/vega-lite/v5.json", + "__theme__": "datawrapper", + "__design__": [ + "The label moved off the axis and onto the bar. Five of these ten names are longer than the bar they belong to, and Flint's y-axis truncates every one of them — 'Cerebrovascular disea…', 'Nephritis, nephrotic …', 'Intentional self-harm…'. A category axis reserves a fixed gutter and clips whatever exceeds it; setting each name on its own line above its bar gives the text the full width of the chart and nothing needs abbreviating.", + "Which means the layout is driven by type, not by the bars. Row height is set by two lines of 11pt text plus the bar, not by dividing the plot height by ten — so the chart grows downward with the length of the list rather than squeezing.", + "Values printed at the end of each bar. The axis can be read to the nearest hundred thousand at best, and the bottom three causes differ by six thousand deaths, which no gridline will ever resolve.", + "One measure, one colour: every bar in the house ink #18A1CD. Flint already uses a single hue here; what changes is that the hue is now the theme's, so this chart sits inside a story with the others.", + "Row order left exactly as the baseline has it — the source arrived ranked, so the bars already descend without a theme touching the sort.", + "Gridlines dashed #DCDCDC behind the bars, no frame, no axis titles — the subtitle already states the unit, so repeating 'Deaths (thousands)' under the axis is the same words twice.", + "11pt type floor and a hairline footer rule." + ], + "background": "#ffffff", + "padding": { "left": 8, "top": 6, "right": 16, "bottom": 8 }, + "title": { + "text": "What Americans die of", + "subtitle": ["Leading causes of death, United States, 2022, thousands of deaths"] + }, + "spacing": 8, + "vconcat": [ + { + "width": 300, + "height": 340, + "data": { + "values": [ + { "Cause": "Diseases of heart", "Deaths (thousands)": 703 }, + { "Cause": "Malignant neoplasms", "Deaths (thousands)": 608 }, + { "Cause": "Unintentional injuries", "Deaths (thousands)": 227 }, + { "Cause": "Cerebrovascular diseases", "Deaths (thousands)": 165 }, + { "Cause": "Chronic lower respiratory diseases", "Deaths (thousands)": 148 }, + { "Cause": "Alzheimer disease", "Deaths (thousands)": 120 }, + { "Cause": "Diabetes mellitus", "Deaths (thousands)": 102 }, + { "Cause": "Nephritis, nephrotic syndrome and nephrosis", "Deaths (thousands)": 58 }, + { "Cause": "Chronic liver disease and cirrhosis", "Deaths (thousands)": 55 }, + { "Cause": "Intentional self-harm (suicide)", "Deaths (thousands)": 49 } + ] + }, + "encoding": { + "y": { + "field": "Cause", + "type": "nominal", + "title": null, + "sort": null, + "axis": null + }, + "x": { + "field": "Deaths (thousands)", + "type": "quantitative", + "title": null, + "scale": { "zero": true, "nice": false, "domain": [0, 760] }, + "axis": { + "values": [0, 200, 400, 600], + "grid": true, + "gridColor": "#dcdcdc", + "gridDash": [2, 2], + "domain": false, + "ticks": false, + "labelPadding": 4 + } + } + }, + "layer": [ + { + "mark": { + "type": "bar", + "color": "#18a1cd", + "height": { "band": 0.34 } + } + }, + { + "mark": { + "type": "text", + "align": "left", + "baseline": "alphabetic", + "dy": -11, + "fontSize": 11, + "color": "#333333" + }, + "encoding": { + "x": { "datum": 0 }, + "text": { "field": "Cause", "type": "nominal" } + } + }, + { + "mark": { + "type": "text", + "align": "left", + "baseline": "middle", + "dx": 6, + "fontSize": 11, + "fontWeight": 600, + "color": "#333333" + }, + "encoding": { + "text": { "field": "Deaths (thousands)", "type": "quantitative", "format": "d" } + } + } + ] + }, + { + "data": { "values": [{}] }, + "mark": { "type": "rect", "fill": "#dcdcdc", "stroke": null }, + "width": 320, + "height": 1, + "view": { "stroke": null } + } + ], + "config": { + "background": "#ffffff", + "font": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "title": { + "font": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "fontSize": 14.5, + "fontWeight": 700, + "color": "#333333", + "anchor": "start", + "offset": 12, + "lineHeight": 18, + "subtitleFont": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "subtitleFontSize": 11.5, + "subtitleColor": "#666666", + "subtitlePadding": 7, + "subtitleLineHeight": 15 + }, + "view": { "stroke": null }, + "axis": { + "labelFont": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "labelFontSize": 11, + "labelColor": "#333333", + "labelPadding": 5, + "grid": false, + "domain": false, + "ticks": false + } + } +} diff --git a/site/src/playground/theme-lab-assets/causes-death.economist.json b/site/src/playground/theme-lab-assets/causes-death.economist.json new file mode 100644 index 00000000..ea282601 --- /dev/null +++ b/site/src/playground/theme-lab-assets/causes-death.economist.json @@ -0,0 +1,105 @@ +{ + "$schema": "https://vega-lite.github.io/schema/vega-lite/v5.json", + "__theme__": "economist", + "__design__": [ + "The category gutter is uncapped rather than widened by hand. Flint reserves a fixed gutter and clips five of the ten names to fit it — 'Cerebrovascular disea…', 'Nephritis, nephrotic …', 'Intentional self-harm…'. Setting 'labelLimit: 0' lets the axis claim what the strings actually need; the bars give up about a third of the frame to pay for it, which on a list whose longest name runs forty-three characters is the correct trade.", + "Value axis moved to the top over a #C9D3DA grid, the same treatment this language uses on every other bar chart here. The reader meets the scale before the bars rather than after them, and on a ranked list the eye is travelling downward, so a bottom axis is read last or not at all.", + "No number printed on any bar. This chart therefore cannot separate chronic liver disease at 55 from self-harm at 49 — six thousand deaths, about a pixel of bar. That is a real loss, taken deliberately: the ranking is what the chart is for, and ten numbers would double the type on a page that is already mostly type.", + "One measure, one ink: every bar in #006BA2. The house red is spent on the masthead tab and nothing else, so it never competes with the data.", + "Row order left exactly as the baseline has it — the source arrived ranked, so the bars descend without a theme touching the sort.", + "Red 26×3 masthead tab above the title, 9.5pt labels, no frame, no axis titles: the subtitle already states the unit, so repeating it under an axis would be the same words twice." + ], + "background": "#ffffff", + "padding": { "left": 8, "top": 6, "right": 16, "bottom": 8 }, + "title": { + "text": "What Americans die of", + "subtitle": ["Leading causes of death, United States, 2022, thousands of deaths"] + }, + "spacing": 7, + "vconcat": [ + { + "data": { "values": [{}] }, + "mark": { "type": "rect", "fill": "#e3120b", "stroke": null }, + "width": 26, + "height": 3, + "view": { "stroke": null } + }, + { + "width": 260, + "height": 235, + "mark": { "type": "bar", "color": "#006ba2", "height": { "band": 0.68 } }, + "data": { + "values": [ + { "Cause": "Diseases of heart", "Deaths (thousands)": 703 }, + { "Cause": "Malignant neoplasms", "Deaths (thousands)": 608 }, + { "Cause": "Unintentional injuries", "Deaths (thousands)": 227 }, + { "Cause": "Cerebrovascular diseases", "Deaths (thousands)": 165 }, + { "Cause": "Chronic lower respiratory diseases", "Deaths (thousands)": 148 }, + { "Cause": "Alzheimer disease", "Deaths (thousands)": 120 }, + { "Cause": "Diabetes mellitus", "Deaths (thousands)": 102 }, + { "Cause": "Nephritis, nephrotic syndrome and nephrosis", "Deaths (thousands)": 58 }, + { "Cause": "Chronic liver disease and cirrhosis", "Deaths (thousands)": 55 }, + { "Cause": "Intentional self-harm (suicide)", "Deaths (thousands)": 49 } + ] + }, + "encoding": { + "y": { + "field": "Cause", + "type": "nominal", + "title": null, + "sort": null, + "axis": { + "labelLimit": 0, + "labelFontSize": 9.5, + "labelColor": "#121317", + "labelPadding": 4, + "domain": false, + "ticks": false, + "grid": false + } + }, + "x": { + "field": "Deaths (thousands)", + "type": "quantitative", + "title": null, + "scale": { "zero": true, "nice": false, "domain": [0, 760] }, + "axis": { + "orient": "top", + "values": [0, 200, 400, 600], + "format": "d", + "grid": true, + "gridColor": "#c9d3da", + "domain": false, + "ticks": false, + "labelPadding": 2 + } + } + } + } + ], + "config": { + "background": "#ffffff", + "font": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "title": { + "font": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "fontSize": 13.5, + "fontWeight": 700, + "color": "#121317", + "anchor": "start", + "offset": 10, + "subtitleFont": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "subtitleFontSize": 10.5, + "subtitleColor": "#54585a", + "subtitlePadding": 6 + }, + "view": { "stroke": null }, + "axis": { + "labelFont": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "labelFontSize": 9.5, + "labelColor": "#54585a", + "grid": false, + "domain": false, + "ticks": false + } + } +} diff --git a/site/src/playground/theme-lab-assets/causes-death.flint.json b/site/src/playground/theme-lab-assets/causes-death.flint.json new file mode 100644 index 00000000..8d23d9a5 --- /dev/null +++ b/site/src/playground/theme-lab-assets/causes-death.flint.json @@ -0,0 +1,95 @@ +{ + "mark": "bar", + "encoding": { + "y": { + "field": "Cause", + "type": "nominal", + "sort": null + }, + "x": { + "field": "Deaths (thousands)", + "type": "quantitative", + "scale": { + "zero": true + }, + "axis": { + "format": ",.12~g" + } + } + }, + "config": { + "view": { + "continuousWidth": 280, + "continuousHeight": 230 + }, + "axisX": { + "labelLimit": 100, + "labelFontSize": 10, + "titleFontSize": 11 + }, + "axisY": { + "labelFontSize": 10, + "titleFontSize": 11 + }, + "legend": { + "labelFontSize": 10, + "titleFontSize": 11 + }, + "facet": { + "spacing": 23 + } + }, + "height": { + "step": 23 + }, + "data": { + "values": [ + { + "Cause": "Diseases of heart", + "Deaths (thousands)": 703 + }, + { + "Cause": "Malignant neoplasms", + "Deaths (thousands)": 608 + }, + { + "Cause": "Unintentional injuries", + "Deaths (thousands)": 227 + }, + { + "Cause": "Cerebrovascular diseases", + "Deaths (thousands)": 165 + }, + { + "Cause": "Chronic lower respiratory diseases", + "Deaths (thousands)": 148 + }, + { + "Cause": "Alzheimer disease", + "Deaths (thousands)": 120 + }, + { + "Cause": "Diabetes mellitus", + "Deaths (thousands)": 102 + }, + { + "Cause": "Nephritis, nephrotic syndrome and nephrosis", + "Deaths (thousands)": 58 + }, + { + "Cause": "Chronic liver disease and cirrhosis", + "Deaths (thousands)": 55 + }, + { + "Cause": "Intentional self-harm (suicide)", + "Deaths (thousands)": 49 + } + ] + }, + "title": { + "text": "What Americans die of", + "subtitle": [ + "Leading causes of death, United States, 2022, thousands of deaths" + ] + } +} diff --git a/site/src/playground/theme-lab-assets/causes-death.mckinsey.json b/site/src/playground/theme-lab-assets/causes-death.mckinsey.json new file mode 100644 index 00000000..3e07da82 --- /dev/null +++ b/site/src/playground/theme-lab-assets/causes-death.mckinsey.json @@ -0,0 +1,103 @@ +{ + "$schema": "https://vega-lite.github.io/schema/vega-lite/v5.json", + "__theme__": "mckinsey", + "__design__": [ + "Values set in a right-aligned column, not floating at the end of each bar. The Economist row on this same data prints the number where the bar stops, which leaves a ragged edge of digits stepping in from 703 down to 49. Locking them to a common right margin makes the figures a readable column in their own right — the reader can scan the numbers without reading the bars at all, which is what a deck page is for.", + "The value axis is deleted and replaced by a single vertical rule at zero. Flint's axis reads to about a hundred thousand deaths, so it cannot separate the bottom three causes — 55 and 49 are six thousand apart. With every value printed the axis has no remaining job; the zero rule is kept because without it the bars have no common origin to be judged against.", + "The category gutter is uncapped. Flint clips five of the ten names to a fixed width — 'Cerebrovascular disea…', 'Nephritis, nephrotic …', 'Intentional self-harm…'. In a document that will be read once, in a meeting, an abbreviation the reader has to decode is a failure.", + "One measure, one ink: every bar in #051C2C. The house blue #2251FF is held in reserve for charts that need a second series, so this page spends no colour at all on a distinction that does not exist in the data.", + "Row order left exactly as the baseline has it — the source arrived ranked, so the bars descend without a theme touching the sort." + ], + "background": "#ffffff", + "padding": { "left": 8, "top": 6, "right": 14, "bottom": 8 }, + "title": { + "text": "What Americans die of", + "subtitle": ["Leading causes of death, United States, 2022, thousands of deaths"] + }, + "width": 250, + "height": 235, + "data": { + "values": [ + { "Cause": "Diseases of heart", "Deaths (thousands)": 703 }, + { "Cause": "Malignant neoplasms", "Deaths (thousands)": 608 }, + { "Cause": "Unintentional injuries", "Deaths (thousands)": 227 }, + { "Cause": "Cerebrovascular diseases", "Deaths (thousands)": 165 }, + { "Cause": "Chronic lower respiratory diseases", "Deaths (thousands)": 148 }, + { "Cause": "Alzheimer disease", "Deaths (thousands)": 120 }, + { "Cause": "Diabetes mellitus", "Deaths (thousands)": 102 }, + { "Cause": "Nephritis, nephrotic syndrome and nephrosis", "Deaths (thousands)": 58 }, + { "Cause": "Chronic liver disease and cirrhosis", "Deaths (thousands)": 55 }, + { "Cause": "Intentional self-harm (suicide)", "Deaths (thousands)": 49 } + ] + }, + "encoding": { + "y": { + "field": "Cause", + "type": "nominal", + "title": null, + "sort": null, + "axis": { + "labelLimit": 0, + "labelFontSize": 10, + "labelColor": "#051c2c", + "labelPadding": 8, + "domain": false, + "ticks": false, + "grid": false + } + }, + "x": { + "field": "Deaths (thousands)", + "type": "quantitative", + "title": null, + "scale": { "zero": true, "nice": false, "domain": [0, 880] }, + "axis": null + } + }, + "layer": [ + { "mark": { "type": "bar", "color": "#051c2c", "height": { "band": 0.6 } } }, + { + "mark": { + "type": "text", + "align": "right", + "baseline": "middle", + "fontSize": 10, + "fontWeight": 600, + "color": "#051c2c" + }, + "encoding": { + "x": { "datum": 880 }, + "text": { "field": "Deaths (thousands)", "type": "quantitative", "format": "d" } + } + }, + { + "mark": { "type": "rule", "color": "#051c2c", "strokeWidth": 1 }, + "encoding": { "x": { "datum": 0 }, "y": { "value": 0 }, "y2": { "value": 235 } } + } + ], + "config": { + "background": "#ffffff", + "font": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "title": { + "font": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "fontSize": 14, + "fontWeight": 700, + "color": "#051c2c", + "anchor": "start", + "offset": 12, + "subtitleFont": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "subtitleFontSize": 10.5, + "subtitleColor": "#5c6b75", + "subtitlePadding": 6 + }, + "view": { "stroke": null }, + "axis": { + "labelFont": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "labelFontSize": 10, + "labelColor": "#051c2c", + "grid": false, + "domain": false, + "ticks": false + } + } +} diff --git a/site/src/playground/theme-lab-assets/causes-death.nature.json b/site/src/playground/theme-lab-assets/causes-death.nature.json new file mode 100644 index 00000000..40208b4d --- /dev/null +++ b/site/src/playground/theme-lab-assets/causes-death.nature.json @@ -0,0 +1,99 @@ +{ + "$schema": "https://vega-lite.github.io/schema/vega-lite/v5.json", + "__theme__": "nature", + "__design__": [ + "The axis title is restored, not removed. Every other language here reasons that the subtitle already names the unit and drops 'Deaths (thousands)' from the axis. A journal figure is reproduced at column width, cited, and lifted out of the article it came from, so the axis must state its own unit — this is the one case where redundant labelling is the correct decision rather than the lazy one.", + "Domain line and outward ticks kept, gridlines refused. Flint draws neither a domain nor ticks; a figure that will be printed at 85 mm needs the scale anchored by a rule the reader can sight along, and gridlines crossing the bars would add ten horizontal interruptions to a panel that is already dense.", + "Type dropped to 8.5pt and the category gutter uncapped. Flint truncates five of ten names — 'Cerebrovascular disea…', 'Nephritis, nephrotic …', 'Intentional self-harm…'. An abbreviated cause of death is not a cause of death, and at journal sizes the smaller face is what makes room for the full string without the panel growing.", + "Bars narrowed to 55% of the band. At this scale the gaps do the work of separating rows, so no rule or alternating fill is needed between them.", + "Single fill #0072B2, the blue from the Okabe-Ito set. One measure needs one colour, and choosing it from a colour-vision-safe set costs nothing here but means the figure is already compliant if a second series is ever added.", + "Row order left exactly as the baseline has it — the source arrived ranked, so the bars descend without a theme touching the sort." + ], + "background": "#ffffff", + "padding": { "left": 8, "top": 6, "right": 14, "bottom": 6 }, + "title": { + "text": "What Americans die of", + "subtitle": ["Leading causes of death, United States, 2022, thousands of deaths"] + }, + "width": 215, + "height": 205, + "data": { + "values": [ + { "Cause": "Diseases of heart", "Deaths (thousands)": 703 }, + { "Cause": "Malignant neoplasms", "Deaths (thousands)": 608 }, + { "Cause": "Unintentional injuries", "Deaths (thousands)": 227 }, + { "Cause": "Cerebrovascular diseases", "Deaths (thousands)": 165 }, + { "Cause": "Chronic lower respiratory diseases", "Deaths (thousands)": 148 }, + { "Cause": "Alzheimer disease", "Deaths (thousands)": 120 }, + { "Cause": "Diabetes mellitus", "Deaths (thousands)": 102 }, + { "Cause": "Nephritis, nephrotic syndrome and nephrosis", "Deaths (thousands)": 58 }, + { "Cause": "Chronic liver disease and cirrhosis", "Deaths (thousands)": 55 }, + { "Cause": "Intentional self-harm (suicide)", "Deaths (thousands)": 49 } + ] + }, + "mark": { "type": "bar", "color": "#0072b2", "height": { "band": 0.55 } }, + "encoding": { + "y": { + "field": "Cause", + "type": "nominal", + "title": null, + "sort": null, + "axis": { + "labelLimit": 0, + "labelFontSize": 8.5, + "labelColor": "#000000", + "labelPadding": 3, + "domain": true, + "domainColor": "#000000", + "ticks": true, + "tickColor": "#000000", + "tickSize": 3, + "grid": false + } + }, + "x": { + "field": "Deaths (thousands)", + "type": "quantitative", + "title": "Deaths (thousands)", + "scale": { "zero": true, "nice": false, "domain": [0, 760] }, + "axis": { + "values": [0, 200, 400, 600], + "format": "d", + "grid": false, + "domain": true, + "domainColor": "#000000", + "ticks": true, + "tickColor": "#000000", + "tickSize": 3.5, + "labelFontSize": 8.5, + "labelColor": "#000000", + "titleFontSize": 9, + "titleFontWeight": 400, + "titleColor": "#000000", + "titlePadding": 4 + } + } + }, + "config": { + "background": "#ffffff", + "font": "Arial, Helvetica, sans-serif", + "title": { + "font": "Arial, Helvetica, sans-serif", + "fontSize": 12, + "fontWeight": 700, + "color": "#000000", + "anchor": "start", + "offset": 6, + "subtitleFont": "Arial, Helvetica, sans-serif", + "subtitleFontSize": 8, + "subtitleFontStyle": "italic", + "subtitleColor": "#3c3c3c", + "subtitlePadding": 5 + }, + "view": { "stroke": null }, + "axis": { + "labelFont": "Arial, Helvetica, sans-serif", + "titleFont": "Arial, Helvetica, sans-serif" + } + } +} diff --git a/site/src/playground/theme-lab-assets/causes-death.nyt.json b/site/src/playground/theme-lab-assets/causes-death.nyt.json new file mode 100644 index 00000000..94d72148 --- /dev/null +++ b/site/src/playground/theme-lab-assets/causes-death.nyt.json @@ -0,0 +1,91 @@ +{ + "$schema": "https://vega-lite.github.io/schema/vega-lite/v5.json", + "__theme__": "nyt", + "__design__": [ + "The value axis is deleted outright and every bar carries its own number. Flint's axis reads to about a hundred thousand deaths at best, which cannot separate the bottom of the list — chronic liver disease at 55 and self-harm at 49 are six thousand deaths apart, roughly a pixel of bar. Ten numbers on ten bars settle that, and they cost less ink than an axis plus a grid.", + "Which leaves no grid to draw. This is the opposite of the Economist row on the same data, which keeps a top axis and refuses per-bar numbers, and the disagreement is the point: both are defensible, and neither follows from the data.", + "The category gutter is given whatever width the names need instead of a fixed one. Flint reserves a gutter and clips what exceeds it — 'Cerebrovascular disea…', 'Nephritis, nephrotic …', 'Intentional self-harm…' — so five of ten rows cannot be read. Setting 'labelLimit: 0' removes the cap; the cost is that roughly two fifths of the frame is now type rather than bar, which is the honest price of a list whose longest name runs forty-three characters.", + "One measure, one ink: every bar in #2F6B9A. The bars are the only saturated element on the page — the type is #121212 and #6B6B6B, and the house red is not spent here because nothing in this chart is being singled out.", + "Row order left exactly as the baseline has it — the source arrived ranked, so the bars descend without a theme touching the sort.", + "Serif headline over sans labels — the house signature, and the only place the two typefaces meet." + ], + "background": "#ffffff", + "padding": { "left": 8, "top": 6, "right": 18, "bottom": 8 }, + "title": { + "text": "What Americans die of", + "subtitle": ["Leading causes of death, United States, 2022, thousands of deaths"] + }, + "width": 260, + "height": 250, + "data": { + "values": [ + { "Cause": "Diseases of heart", "Deaths (thousands)": 703 }, + { "Cause": "Malignant neoplasms", "Deaths (thousands)": 608 }, + { "Cause": "Unintentional injuries", "Deaths (thousands)": 227 }, + { "Cause": "Cerebrovascular diseases", "Deaths (thousands)": 165 }, + { "Cause": "Chronic lower respiratory diseases", "Deaths (thousands)": 148 }, + { "Cause": "Alzheimer disease", "Deaths (thousands)": 120 }, + { "Cause": "Diabetes mellitus", "Deaths (thousands)": 102 }, + { "Cause": "Nephritis, nephrotic syndrome and nephrosis", "Deaths (thousands)": 58 }, + { "Cause": "Chronic liver disease and cirrhosis", "Deaths (thousands)": 55 }, + { "Cause": "Intentional self-harm (suicide)", "Deaths (thousands)": 49 } + ] + }, + "encoding": { + "y": { + "field": "Cause", + "type": "nominal", + "title": null, + "sort": null, + "axis": { + "labelLimit": 0, + "labelFontSize": 10, + "labelColor": "#121212", + "labelPadding": 6, + "domain": false, + "ticks": false, + "grid": false + } + }, + "x": { + "field": "Deaths (thousands)", + "type": "quantitative", + "title": null, + "scale": { "zero": true, "nice": false, "domain": [0, 810] }, + "axis": null + } + }, + "layer": [ + { "mark": { "type": "bar", "color": "#2f6b9a", "height": { "band": 0.62 } } }, + { + "mark": { + "type": "text", + "align": "left", + "baseline": "middle", + "dx": 5, + "fontSize": 10, + "color": "#121212" + }, + "encoding": { + "text": { "field": "Deaths (thousands)", "type": "quantitative", "format": "d" } + } + } + ], + "config": { + "background": "#ffffff", + "font": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "title": { + "font": "Georgia, 'Times New Roman', Times, serif", + "fontSize": 15, + "fontWeight": 700, + "color": "#121212", + "anchor": "start", + "offset": 14, + "subtitleFont": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "subtitleFontSize": 11, + "subtitleColor": "#6b6b6b", + "subtitlePadding": 7 + }, + "view": { "stroke": null } + } +} diff --git a/site/src/playground/theme-lab-assets/causes-death.powerbi.json b/site/src/playground/theme-lab-assets/causes-death.powerbi.json new file mode 100644 index 00000000..1443bf4b --- /dev/null +++ b/site/src/playground/theme-lab-assets/causes-death.powerbi.json @@ -0,0 +1,106 @@ +{ + "$schema": "https://vega-lite.github.io/schema/vega-lite/v5.json", + "__theme__": "powerbi", + "__design__": [ + "Axis and data labels both kept — the only row here that does not choose. Every other language on this data picks one: NYT and Nature trust the axis, the Economist and McKinsey trust the printed number. A dashboard tile is read twice, once across a room and once with a cursor on it, so it has to answer at both distances. The cost is admitted: this is the busiest of the six.", + "Dark ground #1B1A19 and a #118DFF fill. On a light page a saturated blue bar is the heaviest thing on it; on a dark ground the bar is the lightest, so the same ranking reads as emitted rather than printed. The grid drops to #33312F — barely above the ground, because on dark a mid-grey grid glows louder than the bars.", + "The category gutter is uncapped. Flint clips five of the ten names to a fixed width — 'Cerebrovascular disea…', 'Nephritis, nephrotic …', 'Intentional self-harm…'. A tile that truncates is a tile that needs a tooltip to be usable, and a tooltip is not a chart.", + "Labels at 9.5pt in #C8C6C4 rather than white. Pure white type on near-black is the one combination that blooms at low brightness; stepping down to a light grey holds the contrast without the halo.", + "Row order left exactly as the baseline has it — the source arrived ranked, so the bars descend without a theme touching the sort." + ], + "background": "#1b1a19", + "padding": { "left": 10, "top": 8, "right": 16, "bottom": 8 }, + "title": { + "text": "What Americans die of", + "subtitle": ["Leading causes of death, United States, 2022, thousands of deaths"] + }, + "width": 250, + "height": 240, + "data": { + "values": [ + { "Cause": "Diseases of heart", "Deaths (thousands)": 703 }, + { "Cause": "Malignant neoplasms", "Deaths (thousands)": 608 }, + { "Cause": "Unintentional injuries", "Deaths (thousands)": 227 }, + { "Cause": "Cerebrovascular diseases", "Deaths (thousands)": 165 }, + { "Cause": "Chronic lower respiratory diseases", "Deaths (thousands)": 148 }, + { "Cause": "Alzheimer disease", "Deaths (thousands)": 120 }, + { "Cause": "Diabetes mellitus", "Deaths (thousands)": 102 }, + { "Cause": "Nephritis, nephrotic syndrome and nephrosis", "Deaths (thousands)": 58 }, + { "Cause": "Chronic liver disease and cirrhosis", "Deaths (thousands)": 55 }, + { "Cause": "Intentional self-harm (suicide)", "Deaths (thousands)": 49 } + ] + }, + "encoding": { + "y": { + "field": "Cause", + "type": "nominal", + "title": null, + "sort": null, + "axis": { + "labelLimit": 0, + "labelFontSize": 9.5, + "labelColor": "#c8c6c4", + "labelPadding": 6, + "domain": false, + "ticks": false, + "grid": false + } + }, + "x": { + "field": "Deaths (thousands)", + "type": "quantitative", + "title": null, + "scale": { "zero": true, "nice": false, "domain": [0, 810] }, + "axis": { + "values": [0, 200, 400, 600, 800], + "grid": true, + "gridColor": "#3b3a39", + "domain": false, + "ticks": false, + "labelFontSize": 9.5, + "labelColor": "#a19f9d", + "labelPadding": 4 + } + } + }, + "layer": [ + { "mark": { "type": "bar", "color": "#118dff", "height": { "band": 0.6 } } }, + { + "mark": { + "type": "text", + "align": "left", + "baseline": "middle", + "dx": 5, + "fontSize": 9, + "color": "#c8c6c4" + }, + "encoding": { + "text": { "field": "Deaths (thousands)", "type": "quantitative", "format": "d" } + } + } + ], + "config": { + "background": "#1b1a19", + "font": "'Segoe UI', 'Segoe UI Variable', system-ui, -apple-system, sans-serif", + "title": { + "font": "'Segoe UI', 'Segoe UI Variable', system-ui, sans-serif", + "fontSize": 12.5, + "fontWeight": 600, + "color": "#f3f2f1", + "anchor": "start", + "offset": 10, + "subtitleFont": "'Segoe UI', 'Segoe UI Variable', system-ui, sans-serif", + "subtitleFontSize": 10, + "subtitleColor": "#a19f9d", + "subtitlePadding": 5 + }, + "view": { "stroke": null }, + "axis": { + "labelFont": "'Segoe UI', 'Segoe UI Variable', system-ui, sans-serif", + "titleFont": "'Segoe UI', 'Segoe UI Variable', system-ui, sans-serif", + "domain": false, + "ticks": false, + "grid": false + } + } +} diff --git a/site/src/playground/theme-lab-assets/cities-map.flint.json b/site/src/playground/theme-lab-assets/cities-map.flint.json new file mode 100644 index 00000000..8a1b81b4 --- /dev/null +++ b/site/src/playground/theme-lab-assets/cities-map.flint.json @@ -0,0 +1,147 @@ +{ + "layer": [ + { + "mark": { + "type": "geoshape", + "fill": "lightgray", + "stroke": "white" + }, + "data": { + "url": "https://vega.github.io/vega-lite/data/world-110m.json", + "format": { + "type": "topojson", + "feature": "countries" + } + }, + "projection": { + "type": "equalEarth" + } + }, + { + "mark": "circle", + "encoding": { + "longitude": { + "field": "Lon", + "type": "quantitative", + "scale": { + "nice": false + } + }, + "latitude": { + "field": "Lat", + "type": "quantitative", + "scale": { + "nice": false + } + }, + "size": { + "field": "Population (M)", + "type": "quantitative", + "scale": { + "type": "sqrt", + "zero": true, + "range": [ + 9, + 361 + ] + } + } + }, + "projection": { + "type": "equalEarth" + } + } + ], + "width": 600, + "height": 350, + "config": { + "view": { + "continuousWidth": 600, + "continuousHeight": 350, + "stroke": null + }, + "axisX": { + "labelLimit": 100, + "labelFontSize": 10, + "titleFontSize": 11 + }, + "axisY": { + "labelFontSize": 10, + "titleFontSize": 11 + }, + "legend": { + "labelFontSize": 10, + "titleFontSize": 11 + }, + "facet": { + "spacing": 23 + } + }, + "data": { + "values": [ + { + "City": "Tokyo", + "Lon": 139.7, + "Lat": 35.7, + "Population (M)": 37.4 + }, + { + "City": "Delhi", + "Lon": 77.2, + "Lat": 28.6, + "Population (M)": 32.9 + }, + { + "City": "Shanghai", + "Lon": 121.5, + "Lat": 31.2, + "Population (M)": 29.2 + }, + { + "City": "São Paulo", + "Lon": -46.6, + "Lat": -23.5, + "Population (M)": 22.6 + }, + { + "City": "Mexico City", + "Lon": -99.1, + "Lat": 19.4, + "Population (M)": 22.1 + }, + { + "City": "Cairo", + "Lon": 31.2, + "Lat": 30, + "Population (M)": 21.3 + }, + { + "City": "New York", + "Lon": -74, + "Lat": 40.7, + "Population (M)": 18.9 + }, + { + "City": "Lagos", + "Lon": 3.4, + "Lat": 6.5, + "Population (M)": 15.4 + }, + { + "City": "London", + "Lon": -0.1, + "Lat": 51.5, + "Population (M)": 9.5 + }, + { + "City": "Los Angeles", + "Lon": -118.2, + "Lat": 34.1, + "Population (M)": 12.4 + } + ] + }, + "title": { + "text": "World's largest cities (metro population)" + } +} diff --git a/site/src/playground/theme-lab-assets/co2-lollipop.datawrapper.json b/site/src/playground/theme-lab-assets/co2-lollipop.datawrapper.json new file mode 100644 index 00000000..086c49c2 --- /dev/null +++ b/site/src/playground/theme-lab-assets/co2-lollipop.datawrapper.json @@ -0,0 +1,144 @@ +{ + "$schema": "https://vega-lite.github.io/schema/vega-lite/v5.json", + "__theme__": "datawrapper", + "__design__": [ + "Flint's lollipop contradicts itself. The stems are drawn down to y2 = 0 while the y scale carries 'zero: false', so the axis floor lands at 2 and every stem is cut off below it — India's 2.0 has no stem at all and Brazil's is a stub. Either the baseline is zero or it is not; here it is, and the scale says so.", + "Row order left exactly as the baseline has it. China and Germany are both on 8.0 tonnes, and re-sorting by value silently picks a different one to put first — which is the whole argument for keeping order out of a theme's hands: it decides something the data does not.", + "Value labels above each dot. The fact in the subtitle is a range from 37 to 2, an eighteen-fold spread that no reader estimates off a gridline, and the three-way cluster around 8 (Japan 8.5, China 8, Germany 8) is unresolvable without them.", + "Stems demoted to a 1px #C8C8C8 hairline and the ink moved to the dots. Flint draws both at full strength, so the stem — which encodes nothing the dot's height does not already state — carries as much weight as the value it hangs from.", + "Single house colour #18A1CD across all twelve. There is one measure and no category here, so colour stays out of it; a highlight would assert a comparison the subtitle does not make.", + "Flint's ',.12~g' y-axis format — twelve significant digits on a per-capita tonnage — replaced by plain integers, and the axis title dropped because the unit is already in the subtitle.", + "Orientation left vertical, deliberately, to hold the pair aligned. Twelve country names do not fit horizontally at a 24px step, so the labels rotate 45° rather than truncate: a name the reader cannot finish is worse than one they tilt their head for.", + "11pt type floor, dashed #DCDCDC horizontal gridlines, no frame, a near-black x baseline for contrast, and a hairline footer row to close the block." + ], + "background": "#ffffff", + "padding": { "left": 8, "top": 6, "right": 12, "bottom": 8 }, + "title": { + "text": "Emissions per person run from 37 tonnes to 2", + "subtitle": ["Carbon dioxide emissions per capita, 2022, tonnes"] + }, + "spacing": 8, + "vconcat": [ + { + "width": 290, + "height": 175, + "data": { + "values": [ + { "Country": "Qatar", "Tonnes/person": 37 }, + { "Country": "UAE", "Tonnes/person": 22 }, + { "Country": "United States", "Tonnes/person": 15 }, + { "Country": "Canada", "Tonnes/person": 14 }, + { "Country": "Russia", "Tonnes/person": 11 }, + { "Country": "Japan", "Tonnes/person": 8.5 }, + { "Country": "China", "Tonnes/person": 8 }, + { "Country": "Germany", "Tonnes/person": 8 }, + { "Country": "UK", "Tonnes/person": 5 }, + { "Country": "France", "Tonnes/person": 4.6 }, + { "Country": "Brazil", "Tonnes/person": 2.3 }, + { "Country": "India", "Tonnes/person": 2 } + ] + }, + "encoding": { + "x": { + "field": "Country", + "type": "nominal", + "title": null, + "sort": null, + "axis": { + "labelAngle": -45, + "labelAlign": "right", + "grid": false, + "domain": true, + "domainColor": "#333333", + "domainWidth": 1.2, + "ticks": false, + "labelPadding": 5 + } + }, + "y": { + "field": "Tonnes/person", + "type": "quantitative", + "title": null, + "scale": { "zero": true, "nice": false, "domain": [0, 42] }, + "axis": { + "values": [0, 10, 20, 30, 40], + "format": "d", + "grid": true, + "gridColor": "#dcdcdc", + "gridDash": [2, 2], + "domain": false, + "ticks": false + } + } + }, + "layer": [ + { + "mark": { "type": "rule", "color": "#c8c8c8", "strokeWidth": 1 }, + "encoding": { "y2": { "datum": 0 } } + }, + { + "mark": { + "type": "circle", + "size": 80, + "color": "#18a1cd", + "opacity": 1, + "stroke": "#ffffff", + "strokeWidth": 1 + } + }, + { + "mark": { + "type": "text", + "align": "center", + "baseline": "bottom", + "dy": -9, + "fontSize": 10, + "color": "#333333" + }, + "encoding": { + "text": { "field": "Tonnes/person", "type": "quantitative", "format": ".3~g" } + } + } + ] + }, + { + "data": { "values": [{}] }, + "mark": { "type": "rect", "fill": "#dcdcdc", "stroke": null }, + "width": 310, + "height": 1, + "view": { "stroke": null } + } + ], + "config": { + "background": "#ffffff", + "font": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "title": { + "font": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "fontSize": 14.5, + "fontWeight": 700, + "color": "#333333", + "anchor": "start", + "offset": 12, + "lineHeight": 18, + "subtitleFont": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "subtitleFontSize": 11.5, + "subtitleColor": "#666666", + "subtitlePadding": 7, + "subtitleLineHeight": 15 + }, + "view": { "stroke": null }, + "axis": { + "labelFont": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "labelFontSize": 11, + "labelColor": "#333333", + "labelPadding": 5, + "titleFont": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "titleFontSize": 10.5, + "titleFontWeight": 400, + "titleColor": "#767676", + "grid": false, + "domain": false, + "ticks": false + } + } +} diff --git a/site/src/playground/theme-lab-assets/co2-lollipop.flint.json b/site/src/playground/theme-lab-assets/co2-lollipop.flint.json new file mode 100644 index 00000000..0a094fde --- /dev/null +++ b/site/src/playground/theme-lab-assets/co2-lollipop.flint.json @@ -0,0 +1,138 @@ +{ + "encoding": {}, + "layer": [ + { + "mark": { + "type": "rule", + "strokeWidth": 1.5 + }, + "encoding": { + "x": { + "field": "Country", + "type": "nominal", + "sort": null + }, + "y": { + "field": "Tonnes/person", + "type": "quantitative", + "scale": { + "zero": false + }, + "axis": { + "format": ",.12~g" + } + }, + "y2": { + "datum": 0 + } + } + }, + { + "mark": { + "type": "circle", + "size": 80, + "opacity": 1 + }, + "encoding": { + "x": { + "field": "Country", + "type": "nominal", + "sort": null + }, + "y": { + "field": "Tonnes/person", + "type": "quantitative", + "scale": { + "zero": false + }, + "axis": { + "format": ",.12~g" + } + } + } + } + ], + "config": { + "view": { + "continuousWidth": 340, + "continuousHeight": 230 + }, + "axisX": { + "labelLimit": 100, + "labelFontSize": 10, + "titleFontSize": 11 + }, + "axisY": { + "labelFontSize": 10, + "titleFontSize": 11 + }, + "legend": { + "labelFontSize": 10, + "titleFontSize": 11 + }, + "facet": { + "spacing": 23 + } + }, + "width": { + "step": 23 + }, + "data": { + "values": [ + { + "Country": "Qatar", + "Tonnes/person": 37 + }, + { + "Country": "UAE", + "Tonnes/person": 22 + }, + { + "Country": "United States", + "Tonnes/person": 15 + }, + { + "Country": "Canada", + "Tonnes/person": 14 + }, + { + "Country": "Russia", + "Tonnes/person": 11 + }, + { + "Country": "Japan", + "Tonnes/person": 8.5 + }, + { + "Country": "China", + "Tonnes/person": 8 + }, + { + "Country": "Germany", + "Tonnes/person": 8 + }, + { + "Country": "UK", + "Tonnes/person": 5 + }, + { + "Country": "France", + "Tonnes/person": 4.6 + }, + { + "Country": "Brazil", + "Tonnes/person": 2.3 + }, + { + "Country": "India", + "Tonnes/person": 2 + } + ] + }, + "title": { + "text": "Emissions per person run from 37 tonnes to 2", + "subtitle": [ + "Carbon dioxide emissions per capita, 2022, tonnes" + ] + } +} diff --git a/site/src/playground/theme-lab-assets/diamonds.flint.json b/site/src/playground/theme-lab-assets/diamonds.flint.json new file mode 100644 index 00000000..c5aea0fc --- /dev/null +++ b/site/src/playground/theme-lab-assets/diamonds.flint.json @@ -0,0 +1,134 @@ +{ + "mark": "circle", + "encoding": { + "x": { + "field": "Carat", + "type": "quantitative", + "scale": { + "zero": false + }, + "axis": { + "format": ",.12~g" + } + }, + "y": { + "field": "Price (USD)", + "type": "quantitative", + "scale": { + "zero": false + }, + "axis": { + "format": ",.12~g" + } + } + }, + "config": { + "view": { + "continuousWidth": 309, + "continuousHeight": 253 + }, + "axisX": { + "labelLimit": 100, + "labelFontSize": 10, + "titleFontSize": 11 + }, + "axisY": { + "labelFontSize": 10, + "titleFontSize": 11 + }, + "legend": { + "labelFontSize": 10, + "titleFontSize": 11 + }, + "facet": { + "spacing": 25 + } + }, + "data": { + "values": [ + { + "Carat": 0.23, + "Price (USD)": 326 + }, + { + "Carat": 0.21, + "Price (USD)": 326 + }, + { + "Carat": 0.29, + "Price (USD)": 334 + }, + { + "Carat": 0.31, + "Price (USD)": 335 + }, + { + "Carat": 0.24, + "Price (USD)": 336 + }, + { + "Carat": 0.4, + "Price (USD)": 900 + }, + { + "Carat": 0.5, + "Price (USD)": 1500 + }, + { + "Carat": 0.6, + "Price (USD)": 1800 + }, + { + "Carat": 0.7, + "Price (USD)": 2500 + }, + { + "Carat": 0.8, + "Price (USD)": 3000 + }, + { + "Carat": 0.9, + "Price (USD)": 3900 + }, + { + "Carat": 1, + "Price (USD)": 5000 + }, + { + "Carat": 1.01, + "Price (USD)": 5200 + }, + { + "Carat": 1.1, + "Price (USD)": 6000 + }, + { + "Carat": 1.2, + "Price (USD)": 7200 + }, + { + "Carat": 1.3, + "Price (USD)": 8200 + }, + { + "Carat": 1.5, + "Price (USD)": 10000 + }, + { + "Carat": 1.7, + "Price (USD)": 13000 + }, + { + "Carat": 2, + "Price (USD)": 18000 + }, + { + "Carat": 2.5, + "Price (USD)": 25000 + } + ] + }, + "title": { + "text": "Diamonds — carat vs price" + } +} diff --git a/site/src/playground/theme-lab-assets/driving.flint.json b/site/src/playground/theme-lab-assets/driving.flint.json new file mode 100644 index 00000000..41f46591 --- /dev/null +++ b/site/src/playground/theme-lab-assets/driving.flint.json @@ -0,0 +1,343 @@ +{ + "mark": { + "type": "line", + "point": true, + "interpolate": "linear", + "strokeWidth": 2 + }, + "encoding": { + "x": { + "field": "Miles/person", + "type": "quantitative", + "scale": { + "nice": true, + "zero": true + }, + "axis": { + "format": ",.12~g" + } + }, + "y": { + "field": "Gas price", + "type": "quantitative", + "scale": { + "nice": true, + "zero": true + }, + "axis": { + "format": ",.12~g" + } + }, + "order": { + "field": "Year", + "type": "quantitative" + } + }, + "config": { + "view": { + "continuousWidth": 320, + "continuousHeight": 244 + }, + "axisX": { + "labelLimit": 100, + "labelFontSize": 10, + "titleFontSize": 11 + }, + "axisY": { + "labelFontSize": 10, + "titleFontSize": 11 + }, + "legend": { + "labelFontSize": 10, + "titleFontSize": 11 + }, + "facet": { + "spacing": 24 + } + }, + "data": { + "values": [ + { + "Year": "1956", + "Miles/person": 3675, + "Gas price": 2.38 + }, + { + "Year": "1957", + "Miles/person": 3706, + "Gas price": 2.4 + }, + { + "Year": "1958", + "Miles/person": 3766, + "Gas price": 2.26 + }, + { + "Year": "1959", + "Miles/person": 3905, + "Gas price": 2.31 + }, + { + "Year": "1960", + "Miles/person": 3935, + "Gas price": 2.27 + }, + { + "Year": "1961", + "Miles/person": 3977, + "Gas price": 2.25 + }, + { + "Year": "1962", + "Miles/person": 4085, + "Gas price": 2.22 + }, + { + "Year": "1963", + "Miles/person": 4218, + "Gas price": 2.12 + }, + { + "Year": "1964", + "Miles/person": 4369, + "Gas price": 2.11 + }, + { + "Year": "1965", + "Miles/person": 4538, + "Gas price": 2.14 + }, + { + "Year": "1966", + "Miles/person": 4676, + "Gas price": 2.14 + }, + { + "Year": "1967", + "Miles/person": 4827, + "Gas price": 2.14 + }, + { + "Year": "1968", + "Miles/person": 5038, + "Gas price": 2.13 + }, + { + "Year": "1969", + "Miles/person": 5207, + "Gas price": 2.07 + }, + { + "Year": "1970", + "Miles/person": 5376, + "Gas price": 2.01 + }, + { + "Year": "1971", + "Miles/person": 5617, + "Gas price": 1.93 + }, + { + "Year": "1972", + "Miles/person": 5973, + "Gas price": 1.87 + }, + { + "Year": "1973", + "Miles/person": 6154, + "Gas price": 1.9 + }, + { + "Year": "1974", + "Miles/person": 5943, + "Gas price": 2.34 + }, + { + "Year": "1975", + "Miles/person": 6111, + "Gas price": 2.31 + }, + { + "Year": "1976", + "Miles/person": 6389, + "Gas price": 2.32 + }, + { + "Year": "1977", + "Miles/person": 6630, + "Gas price": 2.36 + }, + { + "Year": "1978", + "Miles/person": 6883, + "Gas price": 2.23 + }, + { + "Year": "1979", + "Miles/person": 6744, + "Gas price": 2.68 + }, + { + "Year": "1980", + "Miles/person": 6672, + "Gas price": 3.3 + }, + { + "Year": "1981", + "Miles/person": 6732, + "Gas price": 3.3 + }, + { + "Year": "1982", + "Miles/person": 6835, + "Gas price": 2.92 + }, + { + "Year": "1983", + "Miles/person": 6943, + "Gas price": 2.66 + }, + { + "Year": "1984", + "Miles/person": 7130, + "Gas price": 2.48 + }, + { + "Year": "1985", + "Miles/person": 7323, + "Gas price": 2.36 + }, + { + "Year": "1986", + "Miles/person": 7558, + "Gas price": 1.76 + }, + { + "Year": "1987", + "Miles/person": 7770, + "Gas price": 1.76 + }, + { + "Year": "1988", + "Miles/person": 8089, + "Gas price": 1.68 + }, + { + "Year": "1989", + "Miles/person": 8397, + "Gas price": 1.75 + }, + { + "Year": "1990", + "Miles/person": 8529, + "Gas price": 1.88 + }, + { + "Year": "1991", + "Miles/person": 8535, + "Gas price": 1.78 + }, + { + "Year": "1992", + "Miles/person": 8662, + "Gas price": 1.69 + }, + { + "Year": "1993", + "Miles/person": 8855, + "Gas price": 1.6 + }, + { + "Year": "1994", + "Miles/person": 8909, + "Gas price": 1.59 + }, + { + "Year": "1995", + "Miles/person": 9150, + "Gas price": 1.6 + }, + { + "Year": "1996", + "Miles/person": 9192, + "Gas price": 1.67 + }, + { + "Year": "1997", + "Miles/person": 9416, + "Gas price": 1.65 + }, + { + "Year": "1998", + "Miles/person": 9590, + "Gas price": 1.39 + }, + { + "Year": "1999", + "Miles/person": 9687, + "Gas price": 1.5 + }, + { + "Year": "2000", + "Miles/person": 9717, + "Gas price": 1.89 + }, + { + "Year": "2001", + "Miles/person": 9699, + "Gas price": 1.77 + }, + { + "Year": "2002", + "Miles/person": 9814, + "Gas price": 1.64 + }, + { + "Year": "2003", + "Miles/person": 9868, + "Gas price": 1.86 + }, + { + "Year": "2004", + "Miles/person": 9994, + "Gas price": 2.14 + }, + { + "Year": "2005", + "Miles/person": 10067, + "Gas price": 2.53 + }, + { + "Year": "2006", + "Miles/person": 10037, + "Gas price": 2.79 + }, + { + "Year": "2007", + "Miles/person": 10025, + "Gas price": 2.95 + }, + { + "Year": "2008", + "Miles/person": 9880, + "Gas price": 3.31 + }, + { + "Year": "2009", + "Miles/person": 9657, + "Gas price": 2.38 + }, + { + "Year": "2010", + "Miles/person": 9596, + "Gas price": 2.61 + } + ] + }, + "title": { + "text": "Driving shifts into reverse", + "subtitle": [ + "Miles driven per person against the price of a gallon of gas, United States, 1956–2010" + ] + } +} diff --git a/site/src/playground/theme-lab-assets/driving.nyt.json b/site/src/playground/theme-lab-assets/driving.nyt.json new file mode 100644 index 00000000..c5903978 --- /dev/null +++ b/site/src/playground/theme-lab-assets/driving.nyt.json @@ -0,0 +1,185 @@ +{ + "$schema": "https://vega-lite.github.io/schema/vega-lite/v5.json", + "__theme__": "nyt", + "__design__": [ + "Both zero baselines dropped. Flint anchors each quantity at zero, which is right for a bar but wrong here: a connected scatter is read as a *path*, and forcing both scales to zero squashes fifty-five years of travel into the top-right corner where the loops cannot be seen.", + "Every seventh point, plus the two ends of the path, given its own year label — taken from the Year field the baseline was already using only to order the path. A connected scatter is unreadable without knowing which way time runs, and a legend cannot say that, so the direct labels are load-bearing, not decoration. The rule is positional: no year is picked out for being interesting.", + "Points reduced to small white-filled dots with an accent ring rather than filled markers, so the line stays continuous through them and the labelled years still read as stops.", + "One editorial accent (#c2352b) for the whole path, at 1.8px with round joins. Flint's default blue hairline at 2px reads as a series in a set; here there is only one series and the colour is doing tone, not identity.", + "Frame removed, horizontal gridlines only, black x baseline with outward ticks. Both axes keep a title because neither 'miles per person' nor 'price of a gallon' names itself — the NYT rule deletes self-evident axis titles, not all of them.", + "Y-axis title rotated flat above the axis as a bare unit; serif headline flush left with the source line folded into a sans deck." + ], + "background": "#ffffff", + "padding": { "left": 8, "top": 6, "right": 20, "bottom": 6 }, + "width": 300, + "height": 210, + "title": { + "text": "Driving shifts into reverse", + "subtitle": ["Miles driven per person against the price of a gallon of gas, United States, 1956–2010"] + }, + "data": { + "values": [ + { "Year": "1956", "Miles/person": 3675, "Gas price": 2.38 }, + { "Year": "1957", "Miles/person": 3706, "Gas price": 2.4 }, + { "Year": "1958", "Miles/person": 3766, "Gas price": 2.26 }, + { "Year": "1959", "Miles/person": 3905, "Gas price": 2.31 }, + { "Year": "1960", "Miles/person": 3935, "Gas price": 2.27 }, + { "Year": "1961", "Miles/person": 3977, "Gas price": 2.25 }, + { "Year": "1962", "Miles/person": 4085, "Gas price": 2.22 }, + { "Year": "1963", "Miles/person": 4218, "Gas price": 2.12 }, + { "Year": "1964", "Miles/person": 4369, "Gas price": 2.11 }, + { "Year": "1965", "Miles/person": 4538, "Gas price": 2.14 }, + { "Year": "1966", "Miles/person": 4676, "Gas price": 2.14 }, + { "Year": "1967", "Miles/person": 4827, "Gas price": 2.14 }, + { "Year": "1968", "Miles/person": 5038, "Gas price": 2.13 }, + { "Year": "1969", "Miles/person": 5207, "Gas price": 2.07 }, + { "Year": "1970", "Miles/person": 5376, "Gas price": 2.01 }, + { "Year": "1971", "Miles/person": 5617, "Gas price": 1.93 }, + { "Year": "1972", "Miles/person": 5973, "Gas price": 1.87 }, + { "Year": "1973", "Miles/person": 6154, "Gas price": 1.9 }, + { "Year": "1974", "Miles/person": 5943, "Gas price": 2.34 }, + { "Year": "1975", "Miles/person": 6111, "Gas price": 2.31 }, + { "Year": "1976", "Miles/person": 6389, "Gas price": 2.32 }, + { "Year": "1977", "Miles/person": 6630, "Gas price": 2.36 }, + { "Year": "1978", "Miles/person": 6883, "Gas price": 2.23 }, + { "Year": "1979", "Miles/person": 6744, "Gas price": 2.68 }, + { "Year": "1980", "Miles/person": 6672, "Gas price": 3.3 }, + { "Year": "1981", "Miles/person": 6732, "Gas price": 3.3 }, + { "Year": "1982", "Miles/person": 6835, "Gas price": 2.92 }, + { "Year": "1983", "Miles/person": 6943, "Gas price": 2.66 }, + { "Year": "1984", "Miles/person": 7130, "Gas price": 2.48 }, + { "Year": "1985", "Miles/person": 7323, "Gas price": 2.36 }, + { "Year": "1986", "Miles/person": 7558, "Gas price": 1.76 }, + { "Year": "1987", "Miles/person": 7770, "Gas price": 1.76 }, + { "Year": "1988", "Miles/person": 8089, "Gas price": 1.68 }, + { "Year": "1989", "Miles/person": 8397, "Gas price": 1.75 }, + { "Year": "1990", "Miles/person": 8529, "Gas price": 1.88 }, + { "Year": "1991", "Miles/person": 8535, "Gas price": 1.78 }, + { "Year": "1992", "Miles/person": 8662, "Gas price": 1.69 }, + { "Year": "1993", "Miles/person": 8855, "Gas price": 1.6 }, + { "Year": "1994", "Miles/person": 8909, "Gas price": 1.59 }, + { "Year": "1995", "Miles/person": 9150, "Gas price": 1.6 }, + { "Year": "1996", "Miles/person": 9192, "Gas price": 1.67 }, + { "Year": "1997", "Miles/person": 9416, "Gas price": 1.65 }, + { "Year": "1998", "Miles/person": 9590, "Gas price": 1.39 }, + { "Year": "1999", "Miles/person": 9687, "Gas price": 1.5 }, + { "Year": "2000", "Miles/person": 9717, "Gas price": 1.89 }, + { "Year": "2001", "Miles/person": 9699, "Gas price": 1.77 }, + { "Year": "2002", "Miles/person": 9814, "Gas price": 1.64 }, + { "Year": "2003", "Miles/person": 9868, "Gas price": 1.86 }, + { "Year": "2004", "Miles/person": 9994, "Gas price": 2.14 }, + { "Year": "2005", "Miles/person": 10067, "Gas price": 2.53 }, + { "Year": "2006", "Miles/person": 10037, "Gas price": 2.79 }, + { "Year": "2007", "Miles/person": 10025, "Gas price": 2.95 }, + { "Year": "2008", "Miles/person": 9880, "Gas price": 3.31 }, + { "Year": "2009", "Miles/person": 9657, "Gas price": 2.38 }, + { "Year": "2010", "Miles/person": 9596, "Gas price": 2.61 } + ] + }, + "encoding": { + "x": { + "field": "Miles/person", + "type": "quantitative", + "title": "miles driven per person", + "scale": { "zero": false, "nice": true }, + "axis": { "tickCount": 5, "format": ",.0f", "labelFlush": true } + }, + "y": { + "field": "Gas price", + "type": "quantitative", + "title": "cost of a gallon of gas", + "scale": { "zero": false, "nice": true }, + "axis": { "tickCount": 4, "format": "$.2f" } + }, + "order": { "field": "Year", "type": "quantitative" } + }, + "layer": [ + { + "mark": { + "type": "line", + "color": "#c2352b", + "strokeWidth": 1.8, + "strokeJoin": "round", + "strokeCap": "round" + } + }, + { + "mark": { + "type": "point", + "filled": true, + "size": 14, + "color": "#ffffff", + "stroke": "#c2352b", + "strokeWidth": 1.2 + } + }, + { + "transform": [ + { "filter": "toNumber(datum.Year) % 7 === 0 || datum.Year === '1956' || datum.Year === '2010'" } + ], + "mark": { + "type": "text", + "align": "left", + "baseline": "middle", + "dx": 6, + "dy": -4, + "font": "Georgia, 'Times New Roman', Times, serif", + "fontSize": 9.5, + "color": "#121212" + }, + "encoding": { "text": { "field": "Year", "type": "nominal" } } + } + ], + "config": { + "background": "#ffffff", + "font": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "title": { + "font": "Georgia, 'Times New Roman', Times, serif", + "fontSize": 15, + "fontWeight": 700, + "color": "#121212", + "anchor": "start", + "offset": 14, + "subtitleFont": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "subtitleFontSize": 10.5, + "subtitleColor": "#6b6b6b", + "subtitlePadding": 7 + }, + "view": { "stroke": null }, + "axis": { + "labelFont": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "labelFontSize": 10, + "labelColor": "#6b6b6b", + "titleFont": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "titleFontSize": 10, + "titleFontWeight": 400, + "titleColor": "#8e8e8e", + "domain": false, + "ticks": false, + "grid": false + }, + "axisY": { + "grid": true, + "gridColor": "#e4e4e4", + "gridWidth": 1, + "labelPadding": 4, + "titleAngle": 0, + "titleAlign": "left", + "titleAnchor": "start", + "titleBaseline": "bottom", + "titleY": -8, + "titlePadding": 0 + }, + "axisX": { + "grid": false, + "domain": true, + "domainColor": "#121212", + "domainWidth": 1, + "ticks": true, + "tickColor": "#121212", + "tickSize": 4, + "titleAnchor": "end", + "titlePadding": 4 + } + } +} diff --git a/site/src/playground/theme-lab-assets/earnings-education.flint.json b/site/src/playground/theme-lab-assets/earnings-education.flint.json new file mode 100644 index 00000000..1259f26b --- /dev/null +++ b/site/src/playground/theme-lab-assets/earnings-education.flint.json @@ -0,0 +1,119 @@ +{ + "mark": "bar", + "encoding": { + "x": { + "field": "Weekly earnings ($)", + "type": "quantitative", + "scale": { + "zero": true + }, + "axis": { + "format": ",.12~g" + } + }, + "y": { + "field": "Education", + "type": "nominal", + "sort": null + }, + "color": { + "field": "Sex", + "type": "nominal", + "sort": null, + "scale": { + "scheme": "tableau10" + } + }, + "yOffset": { + "field": "Sex", + "type": "nominal", + "sort": null + } + }, + "config": { + "view": { + "continuousWidth": 340, + "continuousHeight": 230 + }, + "axisX": { + "labelLimit": 100, + "labelFontSize": 10, + "titleFontSize": 11 + }, + "axisY": { + "labelFontSize": 10, + "titleFontSize": 11 + }, + "legend": { + "labelFontSize": 10, + "titleFontSize": 11 + }, + "facet": { + "spacing": 23 + } + }, + "height": { + "step": 46, + "for": "position" + }, + "data": { + "values": [ + { + "Education": "Advanced degree", + "Sex": "Men", + "Weekly earnings ($)": 2160 + }, + { + "Education": "Advanced degree", + "Sex": "Women", + "Weekly earnings ($)": 1600 + }, + { + "Education": "Bachelor's degree", + "Sex": "Men", + "Weekly earnings ($)": 1700 + }, + { + "Education": "Bachelor's degree", + "Sex": "Women", + "Weekly earnings ($)": 1290 + }, + { + "Education": "Some college", + "Sex": "Men", + "Weekly earnings ($)": 1120 + }, + { + "Education": "Some college", + "Sex": "Women", + "Weekly earnings ($)": 890 + }, + { + "Education": "High school", + "Sex": "Men", + "Weekly earnings ($)": 1000 + }, + { + "Education": "High school", + "Sex": "Women", + "Weekly earnings ($)": 790 + }, + { + "Education": "Less than high school", + "Sex": "Men", + "Weekly earnings ($)": 780 + }, + { + "Education": "Less than high school", + "Sex": "Women", + "Weekly earnings ($)": 620 + } + ] + }, + "title": { + "text": "Median weekly earnings by education and sex, 2023", + "subtitle": [ + "US dollars, full-time wage and salary workers" + ] + } +} diff --git a/site/src/playground/theme-lab-assets/earnings-education.mckinsey.json b/site/src/playground/theme-lab-assets/earnings-education.mckinsey.json new file mode 100644 index 00000000..a2ece70d --- /dev/null +++ b/site/src/playground/theme-lab-assets/earnings-education.mckinsey.json @@ -0,0 +1,115 @@ +{ + "$schema": "https://vega-lite.github.io/schema/vega-lite/v5.json", + "__theme__": "mckinsey", + "__design__": [ + "Value axis deleted and every bar labelled directly with its own dollar figure. The comparison the chart is for — the gap inside each rung — is now a subtraction the reader can do in place.", + "Colour cut from tableau10 to the two house inks, navy #051C2C and brand blue #2251FF, at equal weight. The chart is a comparison of two series, so neither may be recessive; what changes is that the hues now come from one palette instead of two arbitrary points on a categorical wheel.", + "Legend kept, but reduced to one unlabelled row of two swatches at the top. With the value axis gone the colours are the only thing naming the series, so dropping the key would leave the chart unreadable; what the briefing format objects to is the boxed, titled legend block, not the key itself.", + "Sub-bar spacing moved onto the offset scale's paddingInner rather than a fixed band fraction on the mark, so every bar stays centred on its own sub-band — a mark-level band height silently pushes the bar off the row its label sits on. Set to 0: the two bars of a pair meet, so each education level reads as one unit and the only gap on the chart is the one between levels.", + "Education labels left as full phrases at 10.5pt instead of being truncated — the category is the argument, so it gets the width.", + "All gridlines, ticks, domains and axis titles removed; the currency format ($,.0f) moves onto the labels so the unit is stated once per mark." + ], + "background": "#ffffff", + "padding": { "left": 8, "top": 6, "right": 34, "bottom": 8 }, + "width": 210, + "title": { + "text": "Median weekly earnings by education and sex, 2023", + "subtitle": ["US dollars, full-time wage and salary workers"] + }, + "height": { "step": 22 }, + "data": { + "values": [ + { "Education": "Less than high school", "Sex": "Men", "Weekly earnings ($)": 780 }, + { "Education": "Less than high school", "Sex": "Women", "Weekly earnings ($)": 620 }, + { "Education": "High school", "Sex": "Men", "Weekly earnings ($)": 1000 }, + { "Education": "High school", "Sex": "Women", "Weekly earnings ($)": 790 }, + { "Education": "Some college", "Sex": "Men", "Weekly earnings ($)": 1120 }, + { "Education": "Some college", "Sex": "Women", "Weekly earnings ($)": 890 }, + { "Education": "Bachelor's degree", "Sex": "Men", "Weekly earnings ($)": 1700 }, + { "Education": "Bachelor's degree", "Sex": "Women", "Weekly earnings ($)": 1290 }, + { "Education": "Advanced degree", "Sex": "Men", "Weekly earnings ($)": 2160 }, + { "Education": "Advanced degree", "Sex": "Women", "Weekly earnings ($)": 1600 } + ] + }, + "encoding": { + "y": { + "field": "Education", + "type": "nominal", + "sort": ["Advanced degree", "Bachelor's degree", "Some college", "High school", "Less than high school"], + "title": null, + "scale": { "paddingInner": 0.25 }, + "axis": { "domain": false, "ticks": false, "grid": false, "labelPadding": 8 } + }, + "yOffset": { + "field": "Sex", + "type": "nominal", + "sort": ["Men", "Women"], + "scale": { "paddingInner": 0 } + }, + "x": { + "field": "Weekly earnings ($)", + "type": "quantitative", + "title": null, + "scale": { "zero": true, "domain": [0, 2600] }, + "axis": null + }, + "color": { + "field": "Sex", + "type": "nominal", + "title": null, + "sort": ["Men", "Women"], + "scale": { "domain": ["Men", "Women"], "range": ["#051c2c", "#2251ff"] }, + "legend": { + "orient": "top", + "direction": "horizontal", + "symbolType": "square", + "symbolSize": 90, + "columnPadding": 8, + "offset": 6, + "padding": 0 + } + } + }, + "layer": [ + { "mark": { "type": "bar" } }, + { + "mark": { + "type": "text", + "align": "left", + "baseline": "middle", + "dx": 5, + "font": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "fontSize": 9.5, + "fontWeight": 600 + }, + "encoding": { + "text": { "field": "Weekly earnings ($)", "type": "quantitative", "format": "$,.0f" } + } + } + ], + "config": { + "background": "#ffffff", + "font": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "title": { + "font": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "fontSize": 13, + "fontWeight": 700, + "color": "#051c2c", + "anchor": "start", + "offset": 12, + "subtitleFont": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "subtitleFontSize": 10, + "subtitleColor": "#8fa0ab", + "subtitlePadding": 7 + }, + "view": { "stroke": null }, + "axis": { + "labelFont": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "labelFontSize": 10.5, + "labelColor": "#051c2c", + "grid": false, + "domain": false, + "ticks": false + } + } +} diff --git a/site/src/playground/theme-lab-assets/electricity-mix-area.economist.json b/site/src/playground/theme-lab-assets/electricity-mix-area.economist.json new file mode 100644 index 00000000..bb8c1559 --- /dev/null +++ b/site/src/playground/theme-lab-assets/electricity-mix-area.economist.json @@ -0,0 +1,149 @@ +{ + "$schema": "https://vega-lite.github.io/schema/vega-lite/v5.json", + "__theme__": "economist", + "__design__": [ + "Masthead rule added above the plot as a separate vconcat row — brand furniture, no data.", + "Colour reassigned so the ramp reads dirty-to-clean — coal and gas take dark slate and rust, the low-carbon sources take the house blues — instead of tableau10's arbitrary assignment, which interleaved fossil and renewable hues. Which hue lands on which source changes; the order of the bands does not.", + "Stack order pinned to the sequence the baseline already produces (Coal → Gas → Hydro → Nuclear → Wind & solar → Other) rather than left to 'sort: null'. The spec now states the sequence instead of inheriting it from row order — the sequence itself is untouched, because what sits against the baseline is a claim about the data.", + "Y axis moved to the right edge with a 0–100 domain and % labels, which is where the paper puts a value axis so the series' left-hand start is unobstructed.", + "Legend moved from a right-hand column to a single horizontal key above the plot, listed in stack order so the key reads bottom-band-first exactly as the chart is built.", + "Area interpolation left linear but the band edges given 0.4px white strokes, so adjacent fills separate without a legend lookup at small sizes.", + "Gridlines dropped entirely: on a 100% stack the total is constant, so horizontal rules only compete with the band boundaries." + ], + "background": "#ffffff", + "padding": { "left": 6, "top": 4, "right": 8, "bottom": 6 }, + "title": { + "text": "Where the power comes from", + "subtitle": ["World electricity generation by source, % of total"] + }, + "spacing": 10, + "vconcat": [ + { + "data": { "values": [{}] }, + "mark": { "type": "rect", "fill": "#e3120b", "stroke": null }, + "width": 26, + "height": 3, + "view": { "stroke": null } + }, + { + "width": 330, + "height": 200, + "data": { + "values": [ + { "Year": "1990", "Source": "Coal", "Generation (TWh)": 4430 }, + { "Year": "1990", "Source": "Gas", "Generation (TWh)": 1780 }, + { "Year": "1990", "Source": "Hydro", "Generation (TWh)": 2160 }, + { "Year": "1990", "Source": "Nuclear", "Generation (TWh)": 2000 }, + { "Year": "1990", "Source": "Wind & solar", "Generation (TWh)": 10 }, + { "Year": "1990", "Source": "Other", "Generation (TWh)": 1580 }, + { "Year": "2000", "Source": "Coal", "Generation (TWh)": 5990 }, + { "Year": "2000", "Source": "Gas", "Generation (TWh)": 2760 }, + { "Year": "2000", "Source": "Hydro", "Generation (TWh)": 2620 }, + { "Year": "2000", "Source": "Nuclear", "Generation (TWh)": 2590 }, + { "Year": "2000", "Source": "Wind & solar", "Generation (TWh)": 80 }, + { "Year": "2000", "Source": "Other", "Generation (TWh)": 1310 }, + { "Year": "2010", "Source": "Coal", "Generation (TWh)": 8670 }, + { "Year": "2010", "Source": "Gas", "Generation (TWh)": 4760 }, + { "Year": "2010", "Source": "Hydro", "Generation (TWh)": 3440 }, + { "Year": "2010", "Source": "Nuclear", "Generation (TWh)": 2760 }, + { "Year": "2010", "Source": "Wind & solar", "Generation (TWh)": 380 }, + { "Year": "2010", "Source": "Other", "Generation (TWh)": 1520 }, + { "Year": "2020", "Source": "Coal", "Generation (TWh)": 9420 }, + { "Year": "2020", "Source": "Gas", "Generation (TWh)": 6270 }, + { "Year": "2020", "Source": "Hydro", "Generation (TWh)": 4360 }, + { "Year": "2020", "Source": "Nuclear", "Generation (TWh)": 2700 }, + { "Year": "2020", "Source": "Wind & solar", "Generation (TWh)": 2720 }, + { "Year": "2020", "Source": "Other", "Generation (TWh)": 1610 } + ] + }, + "mark": { "type": "area", "stroke": "#ffffff", "strokeWidth": 0.4, "interpolate": "linear" }, + "encoding": { + "x": { + "field": "Year", + "type": "temporal", + "title": null, + "scale": { "type": "utc" }, + "axis": { + "format": "%Y", + "values": ["1990", "2000", "2010", "2020"], + "labelFlush": true, + "grid": false, + "domain": true, + "domainColor": "#121317", + "ticks": true, + "tickColor": "#121317", + "tickSize": 4, + "labelPadding": 3 + } + }, + "y": { + "field": "Generation (TWh)", + "type": "quantitative", + "stack": "normalize", + "title": null, + "axis": { + "orient": "right", + "format": ".0%", + "tickCount": 5, + "grid": false, + "domain": false, + "ticks": false, + "labelPadding": 5 + } + }, + "color": { + "field": "Source", + "type": "nominal", + "title": null, + "sort": ["Coal", "Gas", "Hydro", "Nuclear", "Wind & solar", "Other"], + "scale": { + "domain": ["Coal", "Gas", "Hydro", "Nuclear", "Wind & solar", "Other"], + "range": ["#3f5661", "#a1655a", "#006ba2", "#7ba7b8", "#3ebcd2", "#c8b88a"] + }, + "legend": { + "orient": "top", + "direction": "horizontal", + "symbolType": "square", + "symbolSize": 90, + "columnPadding": 8, + "offset": 4, + "padding": 0 + } + }, + "order": { + "field": "Source", + "type": "nominal", + "sort": ["Coal", "Gas", "Hydro", "Nuclear", "Wind & solar", "Other"] + } + } + } + ], + "config": { + "background": "#ffffff", + "font": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "title": { + "font": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "fontSize": 13.5, + "fontWeight": 700, + "color": "#121317", + "anchor": "start", + "offset": 10, + "subtitleFont": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "subtitleFontSize": 10, + "subtitleColor": "#54585a", + "subtitlePadding": 6 + }, + "view": { "stroke": null }, + "axis": { + "labelFont": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "labelFontSize": 9, + "labelColor": "#54585a" + }, + "legend": { + "labelFont": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "labelFontSize": 9, + "labelColor": "#121317", + "symbolStrokeWidth": 0 + } + } +} diff --git a/site/src/playground/theme-lab-assets/electricity-mix-area.flint.json b/site/src/playground/theme-lab-assets/electricity-mix-area.flint.json new file mode 100644 index 00000000..e3485177 --- /dev/null +++ b/site/src/playground/theme-lab-assets/electricity-mix-area.flint.json @@ -0,0 +1,188 @@ +{ + "mark": "area", + "encoding": { + "x": { + "field": "Year", + "type": "temporal" + }, + "y": { + "field": "Generation (TWh)", + "type": "quantitative", + "stack": "normalize", + "scale": { + "zero": true + }, + "axis": { + "format": ",.12~g" + } + }, + "color": { + "field": "Source", + "type": "nominal", + "sort": null, + "scale": { + "scheme": "tableau10", + "domain": [ + "Coal", + "Gas", + "Hydro", + "Nuclear", + "Wind & solar", + "Other" + ] + } + } + }, + "config": { + "view": { + "continuousWidth": 308, + "continuousHeight": 254 + }, + "axisX": { + "labelLimit": 100, + "labelFontSize": 10, + "titleFontSize": 11 + }, + "axisY": { + "labelFontSize": 10, + "titleFontSize": 11 + }, + "legend": { + "labelFontSize": 10, + "titleFontSize": 11 + }, + "facet": { + "spacing": 25 + } + }, + "data": { + "values": [ + { + "Year": "1990", + "Source": "Coal", + "Generation (TWh)": 4430 + }, + { + "Year": "1990", + "Source": "Gas", + "Generation (TWh)": 1780 + }, + { + "Year": "1990", + "Source": "Hydro", + "Generation (TWh)": 2160 + }, + { + "Year": "1990", + "Source": "Nuclear", + "Generation (TWh)": 2000 + }, + { + "Year": "1990", + "Source": "Wind & solar", + "Generation (TWh)": 10 + }, + { + "Year": "1990", + "Source": "Other", + "Generation (TWh)": 1580 + }, + { + "Year": "2000", + "Source": "Coal", + "Generation (TWh)": 5990 + }, + { + "Year": "2000", + "Source": "Gas", + "Generation (TWh)": 2760 + }, + { + "Year": "2000", + "Source": "Hydro", + "Generation (TWh)": 2620 + }, + { + "Year": "2000", + "Source": "Nuclear", + "Generation (TWh)": 2590 + }, + { + "Year": "2000", + "Source": "Wind & solar", + "Generation (TWh)": 80 + }, + { + "Year": "2000", + "Source": "Other", + "Generation (TWh)": 1310 + }, + { + "Year": "2010", + "Source": "Coal", + "Generation (TWh)": 8670 + }, + { + "Year": "2010", + "Source": "Gas", + "Generation (TWh)": 4760 + }, + { + "Year": "2010", + "Source": "Hydro", + "Generation (TWh)": 3440 + }, + { + "Year": "2010", + "Source": "Nuclear", + "Generation (TWh)": 2760 + }, + { + "Year": "2010", + "Source": "Wind & solar", + "Generation (TWh)": 380 + }, + { + "Year": "2010", + "Source": "Other", + "Generation (TWh)": 1520 + }, + { + "Year": "2020", + "Source": "Coal", + "Generation (TWh)": 9420 + }, + { + "Year": "2020", + "Source": "Gas", + "Generation (TWh)": 6270 + }, + { + "Year": "2020", + "Source": "Hydro", + "Generation (TWh)": 4360 + }, + { + "Year": "2020", + "Source": "Nuclear", + "Generation (TWh)": 2700 + }, + { + "Year": "2020", + "Source": "Wind & solar", + "Generation (TWh)": 2720 + }, + { + "Year": "2020", + "Source": "Other", + "Generation (TWh)": 1610 + } + ] + }, + "title": { + "text": "Where the power comes from", + "subtitle": [ + "World electricity generation by source, % of total" + ] + } +} diff --git a/site/src/playground/theme-lab-assets/electricity-stacked.flint.json b/site/src/playground/theme-lab-assets/electricity-stacked.flint.json new file mode 100644 index 00000000..8f0deeea --- /dev/null +++ b/site/src/playground/theme-lab-assets/electricity-stacked.flint.json @@ -0,0 +1,140 @@ +{ + "mark": "bar", + "encoding": { + "x": { + "field": "Country", + "type": "nominal", + "sort": null + }, + "y": { + "field": "Share", + "type": "quantitative", + "scale": { + "zero": true + }, + "axis": { + "format": ",.12~g" + } + }, + "color": { + "field": "Source", + "type": "nominal", + "sort": null, + "scale": { + "scheme": "tableau10", + "domain": [ + "Nuclear", + "Renewables", + "Fossil" + ] + } + } + }, + "config": { + "view": { + "continuousWidth": 340, + "continuousHeight": 230 + }, + "axisX": { + "labelLimit": 100, + "labelFontSize": 10, + "titleFontSize": 11 + }, + "axisY": { + "labelFontSize": 10, + "titleFontSize": 11 + }, + "legend": { + "labelFontSize": 10, + "titleFontSize": 11 + }, + "facet": { + "spacing": 23 + } + }, + "width": { + "step": 23 + }, + "data": { + "values": [ + { + "Country": "France", + "Source": "Nuclear", + "Share": 65 + }, + { + "Country": "France", + "Source": "Renewables", + "Share": 27 + }, + { + "Country": "France", + "Source": "Fossil", + "Share": 8 + }, + { + "Country": "Germany", + "Source": "Renewables", + "Share": 52 + }, + { + "Country": "Germany", + "Source": "Fossil", + "Share": 45 + }, + { + "Country": "Germany", + "Source": "Nuclear", + "Share": 3 + }, + { + "Country": "United States", + "Source": "Fossil", + "Share": 60 + }, + { + "Country": "United States", + "Source": "Nuclear", + "Share": 18 + }, + { + "Country": "United States", + "Source": "Renewables", + "Share": 22 + }, + { + "Country": "China", + "Source": "Fossil", + "Share": 62 + }, + { + "Country": "China", + "Source": "Renewables", + "Share": 33 + }, + { + "Country": "China", + "Source": "Nuclear", + "Share": 5 + }, + { + "Country": "Brazil", + "Source": "Renewables", + "Share": 89 + }, + { + "Country": "Brazil", + "Source": "Fossil", + "Share": 9 + }, + { + "Country": "Brazil", + "Source": "Nuclear", + "Share": 2 + } + ] + }, + "title": { + "text": "Electricity generation mix by country, 2023 (%)" + } +} diff --git a/site/src/playground/theme-lab-assets/ev-share.datawrapper.json b/site/src/playground/theme-lab-assets/ev-share.datawrapper.json new file mode 100644 index 00000000..7b213009 --- /dev/null +++ b/site/src/playground/theme-lab-assets/ev-share.datawrapper.json @@ -0,0 +1,136 @@ +{ + "$schema": "https://vega-lite.github.io/schema/vega-lite/v5.json", + "__theme__": "datawrapper", + "__design__": [ + "The key is kept but moved above the plot and set inline, reading left to right in the baseline's series order. Flint stacks it vertically down the right edge, which costs about a quarter of the frame on a chart only 300 points wide; running it along the top costs one line of type and puts the country names on the reader's path into the chart rather than beside it.", + "Which is a different answer from the two direct-labelling rows here, and the reason is width, not taste: this language targets an embed in a narrow column, where a label sitting to the right of the last point is the first thing to be clipped.", + "House palette #18A1CD, #E2A233, #C04A4A, #2D8659, pinned to the baseline's series order by an explicit domain. Flint's 'set2' pastels sit at similar lightness, so on four crossing lines they separate by hue alone; these four differ in lightness as well and survive a greyscale print.", + "Dashed #DCDCDC horizontal grid at 25-point steps, no vertical rules, no axis domain, no axis title — the subtitle already gives the unit.", + "Year axis labelled at all four observations. The sampling is uneven and this language would rather show that than round it off.", + "11pt type floor and a hairline footer rule.", + "Series order left exactly as the baseline has it: Norway, China, Germany, United States." + ], + "background": "#ffffff", + "padding": { "left": 8, "top": 6, "right": 16, "bottom": 8 }, + "title": { + "text": "Electric cars as a share of new sales", + "subtitle": ["Norway, China, Germany and the United States, 2018–2023, per cent of new car sales"] + }, + "spacing": 8, + "vconcat": [ + { + "width": 300, + "height": 200, + "data": { + "values": [ + { "Year": "2018", "Country": "Norway", "EV share (%)": 49 }, + { "Year": "2020", "Country": "Norway", "EV share (%)": 75 }, + { "Year": "2022", "Country": "Norway", "EV share (%)": 88 }, + { "Year": "2023", "Country": "Norway", "EV share (%)": 93 }, + { "Year": "2018", "Country": "China", "EV share (%)": 4 }, + { "Year": "2020", "Country": "China", "EV share (%)": 6 }, + { "Year": "2022", "Country": "China", "EV share (%)": 29 }, + { "Year": "2023", "Country": "China", "EV share (%)": 38 }, + { "Year": "2018", "Country": "Germany", "EV share (%)": 2 }, + { "Year": "2020", "Country": "Germany", "EV share (%)": 13 }, + { "Year": "2022", "Country": "Germany", "EV share (%)": 31 }, + { "Year": "2023", "Country": "Germany", "EV share (%)": 25 }, + { "Year": "2018", "Country": "United States", "EV share (%)": 2 }, + { "Year": "2020", "Country": "United States", "EV share (%)": 2 }, + { "Year": "2022", "Country": "United States", "EV share (%)": 8 }, + { "Year": "2023", "Country": "United States", "EV share (%)": 10 } + ] + }, + "mark": { "type": "line", "strokeWidth": 2.2 }, + "encoding": { + "x": { + "field": "Year", + "type": "temporal", + "title": null, + "scale": { "type": "utc" }, + "axis": { + "values": ["2018", "2020", "2022", "2023"], + "format": "%Y", + "grid": false, + "domain": false, + "ticks": false, + "labelPadding": 5 + } + }, + "y": { + "field": "EV share (%)", + "type": "quantitative", + "title": null, + "scale": { "zero": true, "nice": false, "domain": [0, 100] }, + "axis": { + "values": [0, 25, 50, 75, 100], + "format": "d", + "grid": true, + "gridColor": "#dcdcdc", + "gridDash": [2, 2], + "domain": false, + "ticks": false, + "labelPadding": 4 + } + }, + "color": { + "field": "Country", + "type": "nominal", + "title": null, + "scale": { + "domain": ["Norway", "China", "Germany", "United States"], + "range": ["#18a1cd", "#e2a233", "#c04a4a", "#2d8659"] + }, + "legend": { + "orient": "top", + "direction": "horizontal", + "anchor": "start", + "labelFontSize": 11, + "labelColor": "#333333", + "symbolType": "stroke", + "symbolStrokeWidth": 3, + "symbolSize": 90, + "offset": 4, + "padding": 0 + } + } + } + }, + { + "data": { "values": [{}] }, + "mark": { "type": "rect", "fill": "#dcdcdc", "stroke": null }, + "width": 320, + "height": 1, + "view": { "stroke": null } + } + ], + "config": { + "background": "#ffffff", + "font": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "title": { + "font": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "fontSize": 14.5, + "fontWeight": 700, + "color": "#333333", + "anchor": "start", + "offset": 12, + "lineHeight": 18, + "subtitleFont": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "subtitleFontSize": 11.5, + "subtitleColor": "#666666", + "subtitlePadding": 7, + "subtitleLineHeight": 15 + }, + "view": { "stroke": null }, + "axis": { + "labelFont": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "labelFontSize": 11, + "labelColor": "#333333", + "labelPadding": 5, + "grid": false, + "domain": false, + "ticks": false + }, + "legend": { "labelFont": "'Helvetica Neue', Helvetica, Arial, sans-serif" } + } +} diff --git a/site/src/playground/theme-lab-assets/ev-share.economist.json b/site/src/playground/theme-lab-assets/ev-share.economist.json new file mode 100644 index 00000000..086b677e --- /dev/null +++ b/site/src/playground/theme-lab-assets/ev-share.economist.json @@ -0,0 +1,131 @@ +{ + "$schema": "https://vega-lite.github.io/schema/vega-lite/v5.json", + "__theme__": "economist", + "__design__": [ + "The legend is deleted and each line is named at its end — the same conclusion the NYT row reaches on this data, by the same reasoning. Worth recording as a convergence rather than dressing up as a difference: on a ranked bar these two languages disagreed about whether to print numbers, and on four separated lines they do not disagree at all. The design decision is forced by the chart, not by the house.", + "Year axis cut to its endpoints, 2018 and 2023. The series is four unevenly spaced observations, so a tick at every one implies the gaps are equal; labelling only the span states the period and leaves the line to describe the path.", + "Horizontal grid at #C9D3DA in 25-point steps, no vertical rules, no axis domain. The grid is the same weight this language uses on every other time series here, which is what makes the rows comparable at all.", + "Palette drawn from the house set — #006BA2, #3EBCD2, #A1655A, #7BA7B8 — in the baseline's series order, pinned by an explicit colour domain so the assignment cannot drift. The red is spent on the masthead tab and nothing else, so no country reads as flagged.", + "Series order left exactly as the baseline has it: Norway, China, Germany, United States.", + "Red 26×3 masthead tab, 9.5pt labels, no frame: the subtitle already states the unit, so an axis title would be the same words twice." + ], + "background": "#ffffff", + "padding": { "left": 8, "top": 6, "right": 86, "bottom": 8 }, + "title": { + "text": "Electric cars as a share of new sales", + "subtitle": ["Norway, China, Germany and the United States, 2018–2023, per cent of new car sales"] + }, + "spacing": 8, + "vconcat": [ + { + "data": { "values": [{}] }, + "mark": { "type": "rect", "fill": "#e3120b", "stroke": null }, + "width": 26, + "height": 3, + "view": { "stroke": null } + }, + { + "width": 290, + "height": 200, + "data": { + "values": [ + { "Year": "2018", "Country": "Norway", "EV share (%)": 49 }, + { "Year": "2020", "Country": "Norway", "EV share (%)": 75 }, + { "Year": "2022", "Country": "Norway", "EV share (%)": 88 }, + { "Year": "2023", "Country": "Norway", "EV share (%)": 93 }, + { "Year": "2018", "Country": "China", "EV share (%)": 4 }, + { "Year": "2020", "Country": "China", "EV share (%)": 6 }, + { "Year": "2022", "Country": "China", "EV share (%)": 29 }, + { "Year": "2023", "Country": "China", "EV share (%)": 38 }, + { "Year": "2018", "Country": "Germany", "EV share (%)": 2 }, + { "Year": "2020", "Country": "Germany", "EV share (%)": 13 }, + { "Year": "2022", "Country": "Germany", "EV share (%)": 31 }, + { "Year": "2023", "Country": "Germany", "EV share (%)": 25 }, + { "Year": "2018", "Country": "United States", "EV share (%)": 2 }, + { "Year": "2020", "Country": "United States", "EV share (%)": 2 }, + { "Year": "2022", "Country": "United States", "EV share (%)": 8 }, + { "Year": "2023", "Country": "United States", "EV share (%)": 10 } + ] + }, + "encoding": { + "x": { + "field": "Year", + "type": "temporal", + "title": null, + "scale": { "type": "utc" }, + "axis": { + "values": ["2018", "2023"], + "format": "%Y", + "grid": false, + "domain": false, + "ticks": false, + "labelPadding": 4 + } + }, + "y": { + "field": "EV share (%)", + "type": "quantitative", + "title": null, + "scale": { "zero": true, "nice": false, "domain": [0, 100] }, + "axis": { + "values": [0, 25, 50, 75, 100], + "format": "d", + "grid": true, + "gridColor": "#c9d3da", + "domain": false, + "ticks": false + } + }, + "color": { + "field": "Country", + "type": "nominal", + "legend": null, + "scale": { + "domain": ["Norway", "China", "Germany", "United States"], + "range": ["#006ba2", "#3ebcd2", "#a1655a", "#7ba7b8"] + } + } + }, + "layer": [ + { "mark": { "type": "line", "strokeWidth": 2 } }, + { + "transform": [{ "filter": "utcyear(datum.Year) === 2023" }], + "mark": { + "type": "text", + "align": "left", + "baseline": "middle", + "dx": 6, + "fontSize": 9.5, + "fontWeight": 600 + }, + "encoding": { "text": { "field": "Country", "type": "nominal" } } + } + ] + } + ], + "config": { + "background": "#ffffff", + "font": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "title": { + "font": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "fontSize": 13.5, + "fontWeight": 700, + "color": "#121317", + "anchor": "start", + "offset": 10, + "subtitleFont": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "subtitleFontSize": 10.5, + "subtitleColor": "#54585a", + "subtitlePadding": 6 + }, + "view": { "stroke": null }, + "axis": { + "labelFont": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "labelFontSize": 9.5, + "labelColor": "#54585a", + "grid": false, + "domain": false, + "ticks": false + } + } +} diff --git a/site/src/playground/theme-lab-assets/ev-share.flint.json b/site/src/playground/theme-lab-assets/ev-share.flint.json new file mode 100644 index 00000000..02d1b69a --- /dev/null +++ b/site/src/playground/theme-lab-assets/ev-share.flint.json @@ -0,0 +1,139 @@ +{ + "mark": "line", + "encoding": { + "x": { + "field": "Year", + "type": "temporal" + }, + "y": { + "field": "EV share (%)", + "type": "quantitative", + "scale": { + "zero": true + }, + "axis": { + "format": ",.12~g" + } + }, + "color": { + "field": "Country", + "type": "nominal", + "sort": null, + "scale": { + "scheme": "set2" + } + } + }, + "config": { + "view": { + "continuousWidth": 308, + "continuousHeight": 254 + }, + "axisX": { + "labelLimit": 100, + "labelFontSize": 10, + "titleFontSize": 11 + }, + "axisY": { + "labelFontSize": 10, + "titleFontSize": 11 + }, + "legend": { + "labelFontSize": 10, + "titleFontSize": 11 + }, + "facet": { + "spacing": 25 + } + }, + "data": { + "values": [ + { + "Year": "2018", + "Country": "Norway", + "EV share (%)": 49 + }, + { + "Year": "2020", + "Country": "Norway", + "EV share (%)": 75 + }, + { + "Year": "2022", + "Country": "Norway", + "EV share (%)": 88 + }, + { + "Year": "2023", + "Country": "Norway", + "EV share (%)": 93 + }, + { + "Year": "2018", + "Country": "China", + "EV share (%)": 4 + }, + { + "Year": "2020", + "Country": "China", + "EV share (%)": 6 + }, + { + "Year": "2022", + "Country": "China", + "EV share (%)": 29 + }, + { + "Year": "2023", + "Country": "China", + "EV share (%)": 38 + }, + { + "Year": "2018", + "Country": "Germany", + "EV share (%)": 2 + }, + { + "Year": "2020", + "Country": "Germany", + "EV share (%)": 13 + }, + { + "Year": "2022", + "Country": "Germany", + "EV share (%)": 31 + }, + { + "Year": "2023", + "Country": "Germany", + "EV share (%)": 25 + }, + { + "Year": "2018", + "Country": "United States", + "EV share (%)": 2 + }, + { + "Year": "2020", + "Country": "United States", + "EV share (%)": 2 + }, + { + "Year": "2022", + "Country": "United States", + "EV share (%)": 8 + }, + { + "Year": "2023", + "Country": "United States", + "EV share (%)": 10 + } + ] + }, + "title": { + "text": "Electric cars as a share of new sales", + "subtitle": [ + "Norway, China, Germany and the United States, 2018–2023, per cent of new car sales" + ] + } +} diff --git a/site/src/playground/theme-lab-assets/ev-share.mckinsey.json b/site/src/playground/theme-lab-assets/ev-share.mckinsey.json new file mode 100644 index 00000000..06f73090 --- /dev/null +++ b/site/src/playground/theme-lab-assets/ev-share.mckinsey.json @@ -0,0 +1,138 @@ +{ + "$schema": "https://vega-lite.github.io/schema/vega-lite/v5.json", + "__theme__": "mckinsey", + "__design__": [ + "Legend deleted, series named at the line ends, and the 2023 value printed next to the name. This is the deck reading of the same problem the ranked-bar row posed: the number is the deliverable, so it goes on the page rather than being recovered from an axis. Norway 93, China 38, Germany 25, United States 10 — four figures a reader can repeat without looking twice.", + "Which makes the value axis redundant, so it is deleted along with every gridline. What remains is a single baseline rule at zero, kept because four lines rising from different starting points need a common floor to be judged against.", + "Year axis reduced to its four observed points with no domain or ticks. The series is unevenly sampled — 2018, 2020, 2022, 2023 — and a nice-numbered axis would smooth over that.", + "Palette is the house progression navy → blue → cyan → teal, pinned to the baseline's series order. All four carry similar weight: none is greyed back, because nothing in this data licenses singling a country out.", + "Series order left exactly as the baseline has it: Norway, China, Germany, United States." + ], + "background": "#ffffff", + "padding": { "left": 8, "top": 6, "right": 108, "bottom": 8 }, + "title": { + "text": "Electric cars as a share of new sales", + "subtitle": ["Norway, China, Germany and the United States, 2018–2023, per cent of new car sales"] + }, + "width": 280, + "height": 205, + "data": { + "values": [ + { "Year": "2018", "Country": "Norway", "EV share (%)": 49 }, + { "Year": "2020", "Country": "Norway", "EV share (%)": 75 }, + { "Year": "2022", "Country": "Norway", "EV share (%)": 88 }, + { "Year": "2023", "Country": "Norway", "EV share (%)": 93 }, + { "Year": "2018", "Country": "China", "EV share (%)": 4 }, + { "Year": "2020", "Country": "China", "EV share (%)": 6 }, + { "Year": "2022", "Country": "China", "EV share (%)": 29 }, + { "Year": "2023", "Country": "China", "EV share (%)": 38 }, + { "Year": "2018", "Country": "Germany", "EV share (%)": 2 }, + { "Year": "2020", "Country": "Germany", "EV share (%)": 13 }, + { "Year": "2022", "Country": "Germany", "EV share (%)": 31 }, + { "Year": "2023", "Country": "Germany", "EV share (%)": 25 }, + { "Year": "2018", "Country": "United States", "EV share (%)": 2 }, + { "Year": "2020", "Country": "United States", "EV share (%)": 2 }, + { "Year": "2022", "Country": "United States", "EV share (%)": 8 }, + { "Year": "2023", "Country": "United States", "EV share (%)": 10 } + ] + }, + "encoding": { + "x": { + "field": "Year", + "type": "temporal", + "title": null, + "scale": { "type": "utc" }, + "axis": { + "values": ["2018", "2020", "2022", "2023"], + "format": "%Y", + "grid": false, + "domain": false, + "ticks": false, + "labelFontSize": 10, + "labelColor": "#051c2c", + "labelPadding": 6 + } + }, + "y": { + "field": "EV share (%)", + "type": "quantitative", + "title": null, + "scale": { "zero": true, "nice": false, "domain": [0, 100] }, + "axis": null + }, + "color": { + "field": "Country", + "type": "nominal", + "legend": null, + "scale": { + "domain": ["Norway", "China", "Germany", "United States"], + "range": ["#051c2c", "#2251ff", "#00a9f4", "#3ebcb6"] + } + } + }, + "layer": [ + { + "mark": { "type": "rule", "color": "#051c2c", "strokeWidth": 1 }, + "encoding": { "y": { "datum": 0 }, "color": null } + }, + { "mark": { "type": "line", "strokeWidth": 2.2 } }, + { "mark": { "type": "point", "filled": true, "size": 30 } }, + { + "transform": [{ "filter": "utcyear(datum.Year) === 2023" }], + "mark": { + "type": "text", + "align": "left", + "baseline": "middle", + "dx": 8, + "fontSize": 10, + "fontWeight": 600 + }, + "encoding": { + "text": { + "field": "Country", + "type": "nominal" + } + } + }, + { + "transform": [{ "filter": "utcyear(datum.Year) === 2023" }], + "mark": { + "type": "text", + "align": "left", + "baseline": "middle", + "dx": 8, + "dy": 12, + "fontSize": 10, + "fontWeight": 400 + }, + "encoding": { + "text": { "field": "EV share (%)", "type": "quantitative", "format": "d" } + } + } + ], + "config": { + "background": "#ffffff", + "font": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "title": { + "font": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "fontSize": 14, + "fontWeight": 700, + "color": "#051c2c", + "anchor": "start", + "offset": 12, + "subtitleFont": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "subtitleFontSize": 10.5, + "subtitleColor": "#5c6b75", + "subtitlePadding": 6 + }, + "view": { "stroke": null }, + "axis": { + "labelFont": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "labelFontSize": 10, + "labelColor": "#051c2c", + "grid": false, + "domain": false, + "ticks": false + } + } +} diff --git a/site/src/playground/theme-lab-assets/ev-share.nature.json b/site/src/playground/theme-lab-assets/ev-share.nature.json new file mode 100644 index 00000000..a43a5c31 --- /dev/null +++ b/site/src/playground/theme-lab-assets/ev-share.nature.json @@ -0,0 +1,147 @@ +{ + "$schema": "https://vega-lite.github.io/schema/vega-lite/v5.json", + "__theme__": "nature", + "__design__": [ + "The key is kept and each series is given a shape as well as a hue. Both direct-labelling languages here delete the legend; a journal figure cannot, because it is reproduced at column width, printed in greyscale often enough to matter, and read by people for whom #0072B2 and #009E73 may be the same colour. Encoding country twice — colour and marker — is redundant on a good screen and load-bearing everywhere else. The legend then has to exist to decode the shape.", + "Palette is the Okabe-Ito colour-vision-safe set, pinned to the baseline's series order by an explicit domain. Flint's default 'set2' scheme is pastel and its greens and oranges converge under the two commonest forms of colour blindness.", + "Both axes titled and both scales anchored by a domain line with outward ticks. Every other row here drops the axis title because the subtitle names the unit; a figure that will be lifted out of its article and cited on its own cannot rely on the caption being carried with it.", + "No gridlines. At 85 mm the panel is small and the four lines cross twice in the middle years, so horizontal rules would add interruptions exactly where the data is hardest to follow.", + "Type at 8.5pt throughout, which is what makes room for a legend beside a 215-point plot without the figure growing.", + "Series order left exactly as the baseline has it: Norway, China, Germany, United States." + ], + "background": "#ffffff", + "padding": { "left": 8, "top": 6, "right": 12, "bottom": 6 }, + "title": { + "text": "Electric cars as a share of new sales", + "subtitle": ["Norway, China, Germany and the United States, 2018–2023, per cent of new car sales"] + }, + "width": 215, + "height": 175, + "data": { + "values": [ + { "Year": "2018", "Country": "Norway", "EV share (%)": 49 }, + { "Year": "2020", "Country": "Norway", "EV share (%)": 75 }, + { "Year": "2022", "Country": "Norway", "EV share (%)": 88 }, + { "Year": "2023", "Country": "Norway", "EV share (%)": 93 }, + { "Year": "2018", "Country": "China", "EV share (%)": 4 }, + { "Year": "2020", "Country": "China", "EV share (%)": 6 }, + { "Year": "2022", "Country": "China", "EV share (%)": 29 }, + { "Year": "2023", "Country": "China", "EV share (%)": 38 }, + { "Year": "2018", "Country": "Germany", "EV share (%)": 2 }, + { "Year": "2020", "Country": "Germany", "EV share (%)": 13 }, + { "Year": "2022", "Country": "Germany", "EV share (%)": 31 }, + { "Year": "2023", "Country": "Germany", "EV share (%)": 25 }, + { "Year": "2018", "Country": "United States", "EV share (%)": 2 }, + { "Year": "2020", "Country": "United States", "EV share (%)": 2 }, + { "Year": "2022", "Country": "United States", "EV share (%)": 8 }, + { "Year": "2023", "Country": "United States", "EV share (%)": 10 } + ] + }, + "encoding": { + "x": { + "field": "Year", + "type": "temporal", + "title": "Year", + "scale": { "type": "utc" }, + "axis": { + "values": ["2018", "2020", "2022", "2023"], + "format": "%Y", + "grid": false, + "domain": true, + "domainColor": "#000000", + "ticks": true, + "tickColor": "#000000", + "tickSize": 3.5, + "labelFontSize": 8.5, + "labelColor": "#000000", + "titleFontSize": 9, + "titleFontWeight": 400, + "titleColor": "#000000", + "titlePadding": 4 + } + }, + "y": { + "field": "EV share (%)", + "type": "quantitative", + "title": "EV share of new sales (%)", + "scale": { "zero": true, "nice": false, "domain": [0, 100] }, + "axis": { + "values": [0, 25, 50, 75, 100], + "format": "d", + "grid": false, + "domain": true, + "domainColor": "#000000", + "ticks": true, + "tickColor": "#000000", + "tickSize": 3.5, + "labelFontSize": 8.5, + "labelColor": "#000000", + "titleFontSize": 9, + "titleFontWeight": 400, + "titleColor": "#000000", + "titlePadding": 4 + } + }, + "color": { + "field": "Country", + "type": "nominal", + "title": null, + "scale": { + "domain": ["Norway", "China", "Germany", "United States"], + "range": ["#0072b2", "#e69f00", "#009e73", "#cc79a7"] + }, + "legend": { + "orient": "right", + "labelFontSize": 8.5, + "labelColor": "#000000", + "symbolStrokeWidth": 1.2, + "symbolSize": 40, + "offset": 6 + } + }, + "shape": { + "field": "Country", + "type": "nominal", + "title": null, + "scale": { + "domain": ["Norway", "China", "Germany", "United States"], + "range": ["circle", "triangle-up", "square", "diamond"] + }, + "legend": { + "orient": "right", + "labelFontSize": 8.5, + "labelColor": "#000000", + "symbolStrokeWidth": 1.2, + "symbolSize": 40, + "offset": 6 + } + } + }, + "layer": [ + { "mark": { "type": "line", "strokeWidth": 1.2 } }, + { "mark": { "type": "point", "filled": true, "size": 28 } } + ], + "config": { + "background": "#ffffff", + "font": "Arial, Helvetica, sans-serif", + "title": { + "font": "Arial, Helvetica, sans-serif", + "fontSize": 12, + "fontWeight": 700, + "color": "#000000", + "anchor": "start", + "offset": 6, + "subtitleFont": "Arial, Helvetica, sans-serif", + "subtitleFontSize": 8, + "subtitleFontStyle": "italic", + "subtitleColor": "#3c3c3c", + "subtitlePadding": 5 + }, + "view": { "stroke": null }, + "axis": { + "labelFont": "Arial, Helvetica, sans-serif", + "titleFont": "Arial, Helvetica, sans-serif" + }, + "legend": { "labelFont": "Arial, Helvetica, sans-serif" } + } +} diff --git a/site/src/playground/theme-lab-assets/ev-share.nyt.json b/site/src/playground/theme-lab-assets/ev-share.nyt.json new file mode 100644 index 00000000..0af029be --- /dev/null +++ b/site/src/playground/theme-lab-assets/ev-share.nyt.json @@ -0,0 +1,116 @@ +{ + "$schema": "https://vega-lite.github.io/schema/vega-lite/v5.json", + "__theme__": "nyt", + "__design__": [ + "The legend is deleted and each line is named at its own right-hand end. Flint puts a four-swatch key beside the plot, which makes reading the chart a two-step lookup: find the colour, carry it back to the line. With four series that end far apart vertically — 93, 38, 25 and 10 — the label can sit where the line stops and the lookup disappears. This is the same move as the direct value labels on the ranked-bar row, applied to a different channel.", + "Which means colour stops being load-bearing. It still separates the lines where they cross in the middle years, but nothing depends on the reader remembering which hue is which, so the palette can be four muted inks rather than four maximally separated ones.", + "Horizontal grid at #E4E4E4 on the value axis only, at 25-point steps, with the year axis unruled. On a percentage series the question is always 'how high', never 'when' precisely, so the grid runs one way.", + "Points marked at each observation. The series is sampled at 2018, 2020, 2022, 2023 — unevenly — and a bare line implies a continuous reading it cannot support. The dots say where the data actually is.", + "Series order left exactly as the baseline has it, Norway through United States; the colour domain is pinned to that order so the assignment cannot drift.", + "Serif headline over sans labels — the house signature, and the only place the two typefaces meet." + ], + "background": "#ffffff", + "padding": { "left": 8, "top": 6, "right": 86, "bottom": 8 }, + "title": { + "text": "Electric cars as a share of new sales", + "subtitle": ["Norway, China, Germany and the United States, 2018–2023, per cent of new car sales"] + }, + "width": 300, + "height": 210, + "data": { + "values": [ + { "Year": "2018", "Country": "Norway", "EV share (%)": 49 }, + { "Year": "2020", "Country": "Norway", "EV share (%)": 75 }, + { "Year": "2022", "Country": "Norway", "EV share (%)": 88 }, + { "Year": "2023", "Country": "Norway", "EV share (%)": 93 }, + { "Year": "2018", "Country": "China", "EV share (%)": 4 }, + { "Year": "2020", "Country": "China", "EV share (%)": 6 }, + { "Year": "2022", "Country": "China", "EV share (%)": 29 }, + { "Year": "2023", "Country": "China", "EV share (%)": 38 }, + { "Year": "2018", "Country": "Germany", "EV share (%)": 2 }, + { "Year": "2020", "Country": "Germany", "EV share (%)": 13 }, + { "Year": "2022", "Country": "Germany", "EV share (%)": 31 }, + { "Year": "2023", "Country": "Germany", "EV share (%)": 25 }, + { "Year": "2018", "Country": "United States", "EV share (%)": 2 }, + { "Year": "2020", "Country": "United States", "EV share (%)": 2 }, + { "Year": "2022", "Country": "United States", "EV share (%)": 8 }, + { "Year": "2023", "Country": "United States", "EV share (%)": 10 } + ] + }, + "encoding": { + "x": { + "field": "Year", + "type": "temporal", + "title": null, + "scale": { "type": "utc" }, + "axis": { + "values": ["2018", "2020", "2022", "2023"], + "format": "%Y", + "grid": false, + "domain": false, + "ticks": false, + "labelFontSize": 10, + "labelColor": "#6b6b6b", + "labelPadding": 4 + } + }, + "y": { + "field": "EV share (%)", + "type": "quantitative", + "title": null, + "scale": { "zero": true, "nice": false, "domain": [0, 100] }, + "axis": { + "values": [0, 25, 50, 75, 100], + "format": "d", + "grid": true, + "gridColor": "#e4e4e4", + "domain": false, + "ticks": false, + "labelFontSize": 10, + "labelColor": "#6b6b6b" + } + }, + "color": { + "field": "Country", + "type": "nominal", + "legend": null, + "scale": { + "domain": ["Norway", "China", "Germany", "United States"], + "range": ["#2f6b9a", "#c2352b", "#5c8a3c", "#9a6c3f"] + } + } + }, + "layer": [ + { "mark": { "type": "line", "strokeWidth": 2 } }, + { "mark": { "type": "point", "filled": true, "size": 26 } }, + { + "transform": [{ "filter": "utcyear(datum.Year) === 2023" }], + "mark": { + "type": "text", + "align": "left", + "baseline": "middle", + "dx": 7, + "fontSize": 10.5, + "fontWeight": 600 + }, + "encoding": { "text": { "field": "Country", "type": "nominal" } } + } + ], + "config": { + "background": "#ffffff", + "font": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "title": { + "font": "Georgia, 'Times New Roman', Times, serif", + "fontSize": 15, + "fontWeight": 700, + "color": "#121212", + "anchor": "start", + "offset": 14, + "subtitleFont": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "subtitleFontSize": 11, + "subtitleColor": "#6b6b6b", + "subtitlePadding": 7 + }, + "view": { "stroke": null } + } +} diff --git a/site/src/playground/theme-lab-assets/ev-share.powerbi.json b/site/src/playground/theme-lab-assets/ev-share.powerbi.json new file mode 100644 index 00000000..179346b1 --- /dev/null +++ b/site/src/playground/theme-lab-assets/ev-share.powerbi.json @@ -0,0 +1,121 @@ +{ + "$schema": "https://vega-lite.github.io/schema/vega-lite/v5.json", + "__theme__": "powerbi", + "__design__": [ + "The key is kept, on the right, where this language always puts it. A dashboard tile is one visual among several on a canvas the user is filtering, and the series set changes when a slicer moves — a legend that redraws in place survives that, whereas labels pinned to the last point of each line rearrange themselves every time the data changes. The two direct-labelling rows here are authoring a fixed picture; this one is describing a query result.", + "Dark ground #1B1A19 with the grid dropped to #3B3A39. On a light page a grid must be pale enough not to compete with the ink; on a dark one the same problem inverts, and a mid-grey rule reads brighter than the lines it is meant to sit behind.", + "Palette lifted to survive the dark ground — #118DFF, #E66C37, #3BD1C7, #E044A7 — pinned to the baseline's series order. Flint's 'set2' pastels are chosen for white and go muddy at this brightness.", + "Points marked at each observation. The series is sampled unevenly, and on a tile that will be hovered for a tooltip the marker is also the target the cursor is looking for.", + "Labels at 9.5pt in #C8C6C4 rather than white: pure white type on near-black blooms at low brightness, and a light grey holds the contrast without the halo.", + "Series order left exactly as the baseline has it: Norway, China, Germany, United States." + ], + "background": "#1b1a19", + "padding": { "left": 10, "top": 8, "right": 14, "bottom": 8 }, + "title": { + "text": "Electric cars as a share of new sales", + "subtitle": ["Norway, China, Germany and the United States, 2018–2023, per cent of new car sales"] + }, + "width": 265, + "height": 195, + "data": { + "values": [ + { "Year": "2018", "Country": "Norway", "EV share (%)": 49 }, + { "Year": "2020", "Country": "Norway", "EV share (%)": 75 }, + { "Year": "2022", "Country": "Norway", "EV share (%)": 88 }, + { "Year": "2023", "Country": "Norway", "EV share (%)": 93 }, + { "Year": "2018", "Country": "China", "EV share (%)": 4 }, + { "Year": "2020", "Country": "China", "EV share (%)": 6 }, + { "Year": "2022", "Country": "China", "EV share (%)": 29 }, + { "Year": "2023", "Country": "China", "EV share (%)": 38 }, + { "Year": "2018", "Country": "Germany", "EV share (%)": 2 }, + { "Year": "2020", "Country": "Germany", "EV share (%)": 13 }, + { "Year": "2022", "Country": "Germany", "EV share (%)": 31 }, + { "Year": "2023", "Country": "Germany", "EV share (%)": 25 }, + { "Year": "2018", "Country": "United States", "EV share (%)": 2 }, + { "Year": "2020", "Country": "United States", "EV share (%)": 2 }, + { "Year": "2022", "Country": "United States", "EV share (%)": 8 }, + { "Year": "2023", "Country": "United States", "EV share (%)": 10 } + ] + }, + "encoding": { + "x": { + "field": "Year", + "type": "temporal", + "title": null, + "scale": { "type": "utc" }, + "axis": { + "values": ["2018", "2020", "2022", "2023"], + "format": "%Y", + "grid": false, + "domain": false, + "ticks": false, + "labelFontSize": 9.5, + "labelColor": "#c8c6c4", + "labelPadding": 4 + } + }, + "y": { + "field": "EV share (%)", + "type": "quantitative", + "title": null, + "scale": { "zero": true, "nice": false, "domain": [0, 100] }, + "axis": { + "values": [0, 25, 50, 75, 100], + "format": "d", + "grid": true, + "gridColor": "#3b3a39", + "domain": false, + "ticks": false, + "labelFontSize": 9.5, + "labelColor": "#c8c6c4" + } + }, + "color": { + "field": "Country", + "type": "nominal", + "title": null, + "scale": { + "domain": ["Norway", "China", "Germany", "United States"], + "range": ["#118dff", "#e66c37", "#3bd1c7", "#e044a7"] + }, + "legend": { + "orient": "right", + "labelFontSize": 9, + "labelColor": "#c8c6c4", + "symbolType": "stroke", + "symbolStrokeWidth": 3, + "symbolSize": 80, + "offset": 8 + } + } + }, + "layer": [ + { "mark": { "type": "line", "strokeWidth": 2 } }, + { "mark": { "type": "point", "filled": true, "size": 28 } } + ], + "config": { + "background": "#1b1a19", + "font": "'Segoe UI', 'Segoe UI Variable', system-ui, -apple-system, sans-serif", + "title": { + "font": "'Segoe UI', 'Segoe UI Variable', system-ui, sans-serif", + "fontSize": 12.5, + "fontWeight": 600, + "color": "#f3f2f1", + "anchor": "start", + "offset": 10, + "subtitleFont": "'Segoe UI', 'Segoe UI Variable', system-ui, sans-serif", + "subtitleFontSize": 10, + "subtitleColor": "#a19f9d", + "subtitlePadding": 5 + }, + "view": { "stroke": null }, + "axis": { + "labelFont": "'Segoe UI', 'Segoe UI Variable', system-ui, sans-serif", + "titleFont": "'Segoe UI', 'Segoe UI Variable', system-ui, sans-serif", + "domain": false, + "ticks": false, + "grid": false + }, + "legend": { "labelFont": "'Segoe UI', 'Segoe UI Variable', system-ui, sans-serif" } + } +} diff --git a/site/src/playground/theme-lab-assets/exam-ecdf.flint.json b/site/src/playground/theme-lab-assets/exam-ecdf.flint.json new file mode 100644 index 00000000..987fdf56 --- /dev/null +++ b/site/src/playground/theme-lab-assets/exam-ecdf.flint.json @@ -0,0 +1,190 @@ +{ + "mark": { + "type": "line", + "interpolate": "step-after" + }, + "transform": [ + { + "window": [ + { + "op": "count", + "field": "Score", + "as": "__ecdf_count" + } + ], + "sort": [ + { + "field": "Score", + "order": "ascending" + } + ], + "frame": [ + null, + 0 + ] + }, + { + "joinaggregate": [ + { + "op": "count", + "field": "Score", + "as": "__ecdf_total" + } + ] + }, + { + "calculate": "datum['__ecdf_count'] / datum['__ecdf_total']", + "as": "__ecdf" + } + ], + "encoding": { + "x": { + "field": "Score", + "type": "quantitative", + "scale": { + "nice": false, + "zero": false + }, + "title": "Score", + "axis": { + "format": ",.12~g" + } + }, + "y": { + "field": "__ecdf", + "type": "quantitative", + "scale": { + "domain": [ + 0, + 1 + ] + }, + "title": "Cumulative proportion", + "axis": { + "format": ",.12~g" + } + } + }, + "config": { + "view": { + "continuousWidth": 340, + "continuousHeight": 230 + }, + "axisX": { + "labelLimit": 100, + "labelFontSize": 10, + "titleFontSize": 11 + }, + "axisY": { + "labelFontSize": 10, + "titleFontSize": 11 + }, + "legend": { + "labelFontSize": 10, + "titleFontSize": 11 + }, + "facet": { + "spacing": 23 + } + }, + "data": { + "values": [ + { + "Score": 55 + }, + { + "Score": 62 + }, + { + "Score": 68 + }, + { + "Score": 71 + }, + { + "Score": 73 + }, + { + "Score": 74 + }, + { + "Score": 76 + }, + { + "Score": 77 + }, + { + "Score": 78 + }, + { + "Score": 79 + }, + { + "Score": 80 + }, + { + "Score": 81 + }, + { + "Score": 82 + }, + { + "Score": 83 + }, + { + "Score": 84 + }, + { + "Score": 85 + }, + { + "Score": 85 + }, + { + "Score": 86 + }, + { + "Score": 87 + }, + { + "Score": 88 + }, + { + "Score": 88 + }, + { + "Score": 89 + }, + { + "Score": 90 + }, + { + "Score": 91 + }, + { + "Score": 92 + }, + { + "Score": 93 + }, + { + "Score": 94 + }, + { + "Score": 95 + }, + { + "Score": 97 + }, + { + "Score": 99 + } + ] + }, + "title": { + "text": "Empirical distribution of exam scores", + "subtitle": [ + "n = 30; circles mark individual scores" + ] + } +} diff --git a/site/src/playground/theme-lab-assets/exam-ecdf.nature.json b/site/src/playground/theme-lab-assets/exam-ecdf.nature.json new file mode 100644 index 00000000..7bdaf38e --- /dev/null +++ b/site/src/playground/theme-lab-assets/exam-ecdf.nature.json @@ -0,0 +1,129 @@ +{ + "$schema": "https://vega-lite.github.io/schema/vega-lite/v5.json", + "__theme__": "nature", + "__design__": [ + "Step interpolation forced to 'step-after'. An empirical CDF is a right-continuous step function; a straight line between observations asserts values that were never measured.", + "Curve extended to both edges of the score range so the function starts at 0 and reaches 1 inside the plot, using the existing minimum and maximum rather than letting the line float.", + "Y scale pinned to exactly [0, 1] with ticks at the quartiles (0, 0.25, 0.5, 0.75, 1) — the values a reader of an ECDF is actually looking up.", + "Observations added back as small open circles on the step, because an ECDF drawn as a bare curve conceals its own sample size.", + "Grid removed and replaced with black L-spines and outward 3.5px ticks; the axis domain is the frame.", + "Colour reduced to a single black 1.2px stroke — one series needs no hue.", + "Y title rewritten as 'Cumulative proportion' and set flat at 9pt; the axis is a proportion, not a count, and the baseline title left that implicit." + ], + "background": "#ffffff", + "padding": { "left": 4, "top": 4, "right": 8, "bottom": 4 }, + "width": 260, + "height": 200, + "title": { + "text": "Empirical distribution of exam scores", + "subtitle": ["n = 30; circles mark individual scores"] + }, + "data": { + "values": [ + { "Score": 55 }, { "Score": 62 }, { "Score": 68 }, { "Score": 71 }, + { "Score": 73 }, { "Score": 74 }, { "Score": 76 }, { "Score": 77 }, + { "Score": 78 }, { "Score": 79 }, { "Score": 80 }, { "Score": 81 }, + { "Score": 82 }, { "Score": 83 }, { "Score": 84 }, { "Score": 85 }, + { "Score": 85 }, { "Score": 86 }, { "Score": 87 }, { "Score": 88 }, + { "Score": 88 }, { "Score": 89 }, { "Score": 90 }, { "Score": 91 }, + { "Score": 92 }, { "Score": 93 }, { "Score": 94 }, { "Score": 95 }, + { "Score": 97 }, { "Score": 99 } + ] + }, + "transform": [ + { + "window": [{ "op": "count", "field": "Score", "as": "__cume" }], + "sort": [{ "field": "Score", "order": "ascending" }], + "frame": [null, 0] + }, + { "joinaggregate": [{ "op": "count", "field": "Score", "as": "__n" }] }, + { "calculate": "datum.__cume / datum.__n", "as": "Cumulative proportion" } + ], + "encoding": { + "x": { + "field": "Score", + "type": "quantitative", + "title": "Score", + "scale": { "domain": [50, 100], "nice": false }, + "axis": { + "values": [50, 60, 70, 80, 90, 100], + "grid": false, + "domain": true, + "domainColor": "#000000", + "domainWidth": 1, + "ticks": true, + "tickColor": "#000000", + "tickSize": 3.5, + "labelPadding": 2, + "titlePadding": 4 + } + }, + "y": { + "field": "Cumulative proportion", + "type": "quantitative", + "title": "Cumulative proportion", + "scale": { "domain": [0, 1], "nice": false }, + "axis": { + "values": [0, 0.25, 0.5, 0.75, 1], + "format": ".2f", + "grid": false, + "domain": true, + "domainColor": "#000000", + "domainWidth": 1, + "ticks": true, + "tickColor": "#000000", + "tickSize": 3.5, + "labelPadding": 2, + "titlePadding": 4 + } + } + }, + "layer": [ + { + "mark": { + "type": "line", + "interpolate": "step-after", + "color": "#000000", + "strokeWidth": 1.2, + "clip": true + } + }, + { + "mark": { + "type": "point", + "filled": false, + "size": 20, + "stroke": "#000000", + "strokeWidth": 0.9, + "fill": "#ffffff", + "clip": true + } + } + ], + "config": { + "background": "#ffffff", + "font": "Helvetica, Arial, sans-serif", + "title": { + "font": "Helvetica, Arial, sans-serif", + "fontSize": 10.5, + "fontWeight": 700, + "color": "#000000", + "anchor": "start", + "offset": 10, + "subtitleFont": "Helvetica, Arial, sans-serif", + "subtitleFontSize": 8, + "subtitleColor": "#3c3c3c", + "subtitlePadding": 6 + }, + "view": { "stroke": null }, + "axis": { + "labelFont": "Helvetica, Arial, sans-serif", + "labelFontSize": 8.5, + "labelColor": "#000000", + "titleFont": "Helvetica, Arial, sans-serif", + "titleFontSize": 9, + "titleColor": "#000000", + "titleFontWeight": 400 + } + } +} diff --git a/site/src/playground/theme-lab-assets/faithful-density.flint.json b/site/src/playground/theme-lab-assets/faithful-density.flint.json new file mode 100644 index 00000000..5d753b49 --- /dev/null +++ b/site/src/playground/theme-lab-assets/faithful-density.flint.json @@ -0,0 +1,162 @@ +{ + "mark": "area", + "transform": [ + { + "density": "Duration (min)" + } + ], + "encoding": { + "x": { + "field": "value", + "type": "quantitative", + "title": "Duration (min)", + "axis": { + "format": ",.12~g" + } + }, + "y": { + "field": "density", + "type": "quantitative", + "axis": { + "format": ",.12~g" + } + } + }, + "config": { + "view": { + "continuousWidth": 340, + "continuousHeight": 230 + }, + "axisX": { + "labelLimit": 100, + "labelFontSize": 10, + "titleFontSize": 11 + }, + "axisY": { + "labelFontSize": 10, + "titleFontSize": 11 + }, + "legend": { + "labelFontSize": 10, + "titleFontSize": 11 + }, + "facet": { + "spacing": 23 + } + }, + "data": { + "values": [ + { + "Duration (min)": 3.6 + }, + { + "Duration (min)": 1.8 + }, + { + "Duration (min)": 3.333 + }, + { + "Duration (min)": 2.283 + }, + { + "Duration (min)": 4.533 + }, + { + "Duration (min)": 2.883 + }, + { + "Duration (min)": 4.7 + }, + { + "Duration (min)": 3.6 + }, + { + "Duration (min)": 1.95 + }, + { + "Duration (min)": 4.35 + }, + { + "Duration (min)": 1.833 + }, + { + "Duration (min)": 3.917 + }, + { + "Duration (min)": 4.2 + }, + { + "Duration (min)": 1.75 + }, + { + "Duration (min)": 4.7 + }, + { + "Duration (min)": 2.167 + }, + { + "Duration (min)": 1.75 + }, + { + "Duration (min)": 4.8 + }, + { + "Duration (min)": 1.6 + }, + { + "Duration (min)": 4.25 + }, + { + "Duration (min)": 1.8 + }, + { + "Duration (min)": 1.75 + }, + { + "Duration (min)": 3.45 + }, + { + "Duration (min)": 3.067 + }, + { + "Duration (min)": 4.533 + }, + { + "Duration (min)": 3.6 + }, + { + "Duration (min)": 1.967 + }, + { + "Duration (min)": 4.083 + }, + { + "Duration (min)": 3.85 + }, + { + "Duration (min)": 4.433 + }, + { + "Duration (min)": 4.3 + }, + { + "Duration (min)": 4.467 + }, + { + "Duration (min)": 3.367 + }, + { + "Duration (min)": 4.033 + }, + { + "Duration (min)": 3.833 + }, + { + "Duration (min)": 2.017 + } + ] + }, + "title": { + "text": "Old Faithful — eruption duration density" + } +} diff --git a/site/src/playground/theme-lab-assets/faithful-hist.flint.json b/site/src/playground/theme-lab-assets/faithful-hist.flint.json new file mode 100644 index 00000000..92314271 --- /dev/null +++ b/site/src/playground/theme-lab-assets/faithful-hist.flint.json @@ -0,0 +1,156 @@ +{ + "mark": "bar", + "encoding": { + "x": { + "bin": true, + "field": "Duration (min)", + "type": "quantitative" + }, + "y": { + "aggregate": "count" + } + }, + "config": { + "view": { + "continuousWidth": 253, + "continuousHeight": 230 + }, + "axisX": { + "labelLimit": 100, + "labelFontSize": 10, + "titleFontSize": 11, + "labelAngle": -45, + "labelAlign": "right", + "labelBaseline": "top" + }, + "axisY": { + "labelFontSize": 10, + "titleFontSize": 11 + }, + "legend": { + "labelFontSize": 10, + "titleFontSize": 11 + }, + "facet": { + "spacing": 23 + } + }, + "data": { + "values": [ + { + "Duration (min)": 3.6 + }, + { + "Duration (min)": 1.8 + }, + { + "Duration (min)": 3.333 + }, + { + "Duration (min)": 2.283 + }, + { + "Duration (min)": 4.533 + }, + { + "Duration (min)": 2.883 + }, + { + "Duration (min)": 4.7 + }, + { + "Duration (min)": 3.6 + }, + { + "Duration (min)": 1.95 + }, + { + "Duration (min)": 4.35 + }, + { + "Duration (min)": 1.833 + }, + { + "Duration (min)": 3.917 + }, + { + "Duration (min)": 4.2 + }, + { + "Duration (min)": 1.75 + }, + { + "Duration (min)": 4.7 + }, + { + "Duration (min)": 2.167 + }, + { + "Duration (min)": 1.75 + }, + { + "Duration (min)": 4.8 + }, + { + "Duration (min)": 1.6 + }, + { + "Duration (min)": 4.25 + }, + { + "Duration (min)": 1.8 + }, + { + "Duration (min)": 1.75 + }, + { + "Duration (min)": 3.45 + }, + { + "Duration (min)": 3.067 + }, + { + "Duration (min)": 4.533 + }, + { + "Duration (min)": 3.6 + }, + { + "Duration (min)": 1.967 + }, + { + "Duration (min)": 4.083 + }, + { + "Duration (min)": 3.85 + }, + { + "Duration (min)": 4.433 + }, + { + "Duration (min)": 4.3 + }, + { + "Duration (min)": 4.467 + }, + { + "Duration (min)": 3.367 + }, + { + "Duration (min)": 4.033 + }, + { + "Duration (min)": 3.833 + }, + { + "Duration (min)": 2.017 + } + ] + }, + "title": { + "text": "Old Faithful eruption durations", + "subtitle": [ + "Two clusters, not one: short eruptions near 2 min and long ones near 4 min" + ] + } +} diff --git a/site/src/playground/theme-lab-assets/faithful-hist.nature.json b/site/src/playground/theme-lab-assets/faithful-hist.nature.json new file mode 100644 index 00000000..2fefbb52 --- /dev/null +++ b/site/src/playground/theme-lab-assets/faithful-hist.nature.json @@ -0,0 +1,119 @@ +{ + "$schema": "https://vega-lite.github.io/schema/vega-lite/v5.json", + "__theme__": "nature", + "__design__": [ + "Bin width stated rather than inferred. Flint's 'bin: true' hands the choice to d3's default, so the histogram's shape is a function of a heuristic the reader cannot see. Here it is a fixed 0.5-minute step over [1.5, 5], which gives the seven bins the square-root rule recommends for n = 36 — a number that can be written into a methods line and reproduced.", + "n printed on the axis title. A count histogram whose sample size is not stated cannot be compared with any other panel, and 36 observations is small enough that the bimodality claim depends on knowing it.", + "Bars given a white 0.5px separator and a flat Okabe–Ito blue fill. In greyscale the separator is what keeps adjacent bins from merging into one silhouette — the figure has to survive being printed without colour.", + "Frame replaced by black L-shaped spines with outward 3.5pt ticks and no gridlines. A gridline behind a bar is measuring the bar's own edge; the spine and tick do the job with less ink.", + "Both axis titles carry their unit explicitly (minutes, count), which is the one piece of chrome a figure panel is not allowed to drop.", + "Geometry cut to 89 mm column width with 8.5pt labels and 4px padding, so the panel is legible at the size it will actually be printed rather than at the size it was designed." + ], + "background": "#ffffff", + "padding": { "left": 4, "top": 4, "right": 6, "bottom": 4 }, + "width": 230, + "height": 160, + "title": { + "text": "Old Faithful eruption durations", + "subtitle": ["Two clusters, not one: short eruptions near 2 min and long ones near 4 min"] + }, + "data": { + "values": [ + { "Duration (min)": 3.6 }, + { "Duration (min)": 1.8 }, + { "Duration (min)": 3.333 }, + { "Duration (min)": 2.283 }, + { "Duration (min)": 4.533 }, + { "Duration (min)": 2.883 }, + { "Duration (min)": 4.7 }, + { "Duration (min)": 3.6 }, + { "Duration (min)": 1.95 }, + { "Duration (min)": 4.35 }, + { "Duration (min)": 1.833 }, + { "Duration (min)": 3.917 }, + { "Duration (min)": 4.2 }, + { "Duration (min)": 1.75 }, + { "Duration (min)": 4.7 }, + { "Duration (min)": 2.167 }, + { "Duration (min)": 1.75 }, + { "Duration (min)": 4.8 }, + { "Duration (min)": 1.6 }, + { "Duration (min)": 4.25 }, + { "Duration (min)": 1.8 }, + { "Duration (min)": 1.75 }, + { "Duration (min)": 3.45 }, + { "Duration (min)": 3.067 }, + { "Duration (min)": 4.533 }, + { "Duration (min)": 3.6 }, + { "Duration (min)": 1.967 }, + { "Duration (min)": 4.083 }, + { "Duration (min)": 3.85 }, + { "Duration (min)": 4.433 }, + { "Duration (min)": 4.3 }, + { "Duration (min)": 4.467 }, + { "Duration (min)": 3.367 }, + { "Duration (min)": 4.033 }, + { "Duration (min)": 3.833 }, + { "Duration (min)": 2.017 } + ] + }, + "mark": { + "type": "bar", + "color": "#0072b2", + "stroke": "#ffffff", + "strokeWidth": 0.5 + }, + "encoding": { + "x": { + "field": "Duration (min)", + "type": "quantitative", + "bin": { "step": 0.5, "extent": [1.5, 5] }, + "title": "Eruption duration (min)", + "axis": { "format": ".1f", "tickCount": 8, "labelOverlap": "greedy" } + }, + "y": { + "aggregate": "count", + "type": "quantitative", + "title": "Count (n = 36)", + "scale": { "nice": false, "domain": [0, 9] }, + "axis": { "values": [0, 3, 6, 9], "format": "d" } + } + }, + "config": { + "background": "#ffffff", + "font": "Arial, Helvetica, sans-serif", + "title": { + "font": "Arial, Helvetica, sans-serif", + "fontSize": 12, + "fontWeight": 700, + "color": "#000000", + "anchor": "start", + "offset": 6, + "subtitleFont": "Arial, Helvetica, sans-serif", + "subtitleFontSize": 8, + "subtitleFontStyle": "italic", + "subtitleColor": "#3c3c3c", + "subtitlePadding": 5 + }, + "view": { "stroke": null }, + "axis": { + "labelFont": "Arial, Helvetica, sans-serif", + "labelFontSize": 8.5, + "labelColor": "#000000", + "labelPadding": 2, + "titleFont": "Arial, Helvetica, sans-serif", + "titleFontSize": 9, + "titleFontWeight": 400, + "titleColor": "#000000", + "titlePadding": 4, + "grid": false, + "domain": true, + "domainColor": "#000000", + "domainWidth": 1, + "ticks": true, + "tickColor": "#000000", + "tickWidth": 1, + "tickSize": 3.5 + } + } +} diff --git a/site/src/playground/theme-lab-assets/faithful.flint.json b/site/src/playground/theme-lab-assets/faithful.flint.json new file mode 100644 index 00000000..fe88a08f --- /dev/null +++ b/site/src/playground/theme-lab-assets/faithful.flint.json @@ -0,0 +1,198 @@ +{ + "mark": "circle", + "encoding": { + "x": { + "field": "Duration (min)", + "type": "quantitative", + "scale": { + "zero": false + }, + "axis": { + "format": ",.12~g" + } + }, + "y": { + "field": "Waiting (min)", + "type": "quantitative", + "scale": { + "zero": false + }, + "axis": { + "format": ",.12~g" + } + } + }, + "config": { + "view": { + "continuousWidth": 310, + "continuousHeight": 253 + }, + "axisX": { + "labelLimit": 100, + "labelFontSize": 10, + "titleFontSize": 11 + }, + "axisY": { + "labelFontSize": 10, + "titleFontSize": 11 + }, + "legend": { + "labelFontSize": 10, + "titleFontSize": 11 + }, + "facet": { + "spacing": 25 + } + }, + "data": { + "values": [ + { + "Duration (min)": 3.6, + "Waiting (min)": 79 + }, + { + "Duration (min)": 1.8, + "Waiting (min)": 54 + }, + { + "Duration (min)": 3.333, + "Waiting (min)": 74 + }, + { + "Duration (min)": 2.283, + "Waiting (min)": 62 + }, + { + "Duration (min)": 4.533, + "Waiting (min)": 85 + }, + { + "Duration (min)": 2.883, + "Waiting (min)": 55 + }, + { + "Duration (min)": 4.7, + "Waiting (min)": 88 + }, + { + "Duration (min)": 3.6, + "Waiting (min)": 85 + }, + { + "Duration (min)": 1.95, + "Waiting (min)": 51 + }, + { + "Duration (min)": 4.35, + "Waiting (min)": 85 + }, + { + "Duration (min)": 1.833, + "Waiting (min)": 54 + }, + { + "Duration (min)": 3.917, + "Waiting (min)": 84 + }, + { + "Duration (min)": 4.2, + "Waiting (min)": 78 + }, + { + "Duration (min)": 1.75, + "Waiting (min)": 47 + }, + { + "Duration (min)": 4.7, + "Waiting (min)": 83 + }, + { + "Duration (min)": 2.167, + "Waiting (min)": 52 + }, + { + "Duration (min)": 1.75, + "Waiting (min)": 62 + }, + { + "Duration (min)": 4.8, + "Waiting (min)": 84 + }, + { + "Duration (min)": 1.6, + "Waiting (min)": 52 + }, + { + "Duration (min)": 4.25, + "Waiting (min)": 79 + }, + { + "Duration (min)": 1.8, + "Waiting (min)": 51 + }, + { + "Duration (min)": 1.75, + "Waiting (min)": 47 + }, + { + "Duration (min)": 3.45, + "Waiting (min)": 78 + }, + { + "Duration (min)": 3.067, + "Waiting (min)": 69 + }, + { + "Duration (min)": 4.533, + "Waiting (min)": 74 + }, + { + "Duration (min)": 3.6, + "Waiting (min)": 83 + }, + { + "Duration (min)": 1.967, + "Waiting (min)": 55 + }, + { + "Duration (min)": 4.083, + "Waiting (min)": 76 + }, + { + "Duration (min)": 3.85, + "Waiting (min)": 78 + }, + { + "Duration (min)": 4.433, + "Waiting (min)": 79 + }, + { + "Duration (min)": 4.3, + "Waiting (min)": 73 + }, + { + "Duration (min)": 4.467, + "Waiting (min)": 77 + }, + { + "Duration (min)": 3.367, + "Waiting (min)": 66 + }, + { + "Duration (min)": 4.033, + "Waiting (min)": 80 + }, + { + "Duration (min)": 3.833, + "Waiting (min)": 74 + }, + { + "Duration (min)": 2.017, + "Waiting (min)": 52 + } + ] + }, + "title": { + "text": "Old Faithful eruptions — waiting time vs duration" + } +} diff --git a/site/src/playground/theme-lab-assets/fed-funds-step.flint.json b/site/src/playground/theme-lab-assets/fed-funds-step.flint.json new file mode 100644 index 00000000..1c526e24 --- /dev/null +++ b/site/src/playground/theme-lab-assets/fed-funds-step.flint.json @@ -0,0 +1,94 @@ +{ + "mark": { + "type": "line", + "interpolate": "step" + }, + "encoding": { + "x": { + "field": "Year", + "type": "temporal" + }, + "y": { + "field": "Target rate (%)", + "type": "quantitative", + "scale": { + "zero": true + }, + "axis": { + "format": ",.12~g" + } + } + }, + "config": { + "view": { + "continuousWidth": 317, + "continuousHeight": 247 + }, + "axisX": { + "labelLimit": 100, + "labelFontSize": 10, + "titleFontSize": 11 + }, + "axisY": { + "labelFontSize": 10, + "titleFontSize": 11 + }, + "legend": { + "labelFontSize": 10, + "titleFontSize": 11 + }, + "facet": { + "spacing": 25 + } + }, + "data": { + "values": [ + { + "Year": "2015", + "Target rate (%)": 0.5 + }, + { + "Year": "2016", + "Target rate (%)": 0.75 + }, + { + "Year": "2017", + "Target rate (%)": 1.5 + }, + { + "Year": "2018", + "Target rate (%)": 2.5 + }, + { + "Year": "2019", + "Target rate (%)": 1.75 + }, + { + "Year": "2020", + "Target rate (%)": 0.25 + }, + { + "Year": "2021", + "Target rate (%)": 0.25 + }, + { + "Year": "2022", + "Target rate (%)": 4.5 + }, + { + "Year": "2023", + "Target rate (%)": 5.5 + }, + { + "Year": "2024", + "Target rate (%)": 4.75 + } + ] + }, + "title": { + "text": "Federal funds target rate", + "subtitle": [ + "Upper bound at year end, %" + ] + } +} diff --git a/site/src/playground/theme-lab-assets/fed-funds-step.powerbi.json b/site/src/playground/theme-lab-assets/fed-funds-step.powerbi.json new file mode 100644 index 00000000..9438af4c --- /dev/null +++ b/site/src/playground/theme-lab-assets/fed-funds-step.powerbi.json @@ -0,0 +1,128 @@ +{ + "$schema": "https://vega-lite.github.io/schema/vega-lite/v5.json", + "__theme__": "powerbi", + "__design__": [ + "Whole tile inverted to the #1B1A19 dashboard canvas so it sits inside a dark report page instead of punching a white hole in it.", + "Step interpolation kept but pushed to 'step-after', which is the correct semantics for a policy rate: the rate holds until the meeting that changes it, so the vertical must land on the announcement year.", + "Area fill added under the step line as the same accent at 18% opacity. A rate path has a meaningful zero, so the region between the line and zero carries information the bare stroke was throwing away.", + "Accent switched to Power BI's #118DFF at 2.2px with a square line cap, matching the tile's other visuals.", + "Y axis reduced to a 4-tick grid of 1px #3B3A39 rules with no domain and no ticks; the values get a % suffix so the unit lives on the labels instead of an axis title.", + "Latest point emphasised with a filled marker so a glancing read lands on the current rate — this uses only the last row of the same series, no new data.", + "Segoe UI at 9.5pt in #C8C6C4, the dashboard's secondary ink, with the title in #F3F2F1 at 12.5pt." + ], + "background": "#1b1a19", + "padding": { "left": 8, "top": 6, "right": 12, "bottom": 8 }, + "width": 300, + "height": 190, + "title": { + "text": "Federal funds target rate", + "subtitle": ["Upper bound at year end, %"] + }, + "data": { + "values": [ + { "Year": "2015", "Target rate (%)": 0.5 }, + { "Year": "2016", "Target rate (%)": 0.75 }, + { "Year": "2017", "Target rate (%)": 1.5 }, + { "Year": "2018", "Target rate (%)": 2.5 }, + { "Year": "2019", "Target rate (%)": 1.75 }, + { "Year": "2020", "Target rate (%)": 0.25 }, + { "Year": "2021", "Target rate (%)": 0.25 }, + { "Year": "2022", "Target rate (%)": 4.5 }, + { "Year": "2023", "Target rate (%)": 5.5 }, + { "Year": "2024", "Target rate (%)": 4.75 } + ] + }, + "encoding": { + "x": { + "field": "Year", + "type": "temporal", + "title": null, + "scale": { "type": "utc" }, + "axis": { + "format": "%Y", + "tickCount": 5, + "grid": false, + "domain": true, + "domainColor": "#3b3a39", + "ticks": false, + "labelPadding": 6, + "labelColor": "#c8c6c4" + } + }, + "y": { + "field": "Target rate (%)", + "type": "quantitative", + "title": null, + "scale": { "domain": [0, 6] }, + "axis": { + "tickCount": 4, + "format": ".1f", + "labelExpr": "datum.label + '%'", + "grid": true, + "gridColor": "#3b3a39", + "gridWidth": 1, + "domain": false, + "ticks": false, + "labelPadding": 6, + "labelColor": "#c8c6c4" + } + } + }, + "layer": [ + { + "mark": { + "type": "area", + "interpolate": "step-after", + "line": false, + "color": "#118dff", + "opacity": 0.18 + } + }, + { + "mark": { + "type": "line", + "interpolate": "step-after", + "color": "#118dff", + "strokeWidth": 2.2, + "strokeCap": "square", + "strokeJoin": "miter" + } + }, + { + "transform": [ + { "window": [{ "op": "row_number", "as": "__rank" }], "sort": [{ "field": "Year", "order": "descending" }] }, + { "filter": "datum.__rank === 1" } + ], + "mark": { + "type": "point", + "filled": true, + "size": 60, + "color": "#118dff", + "stroke": "#1b1a19", + "strokeWidth": 2 + } + } + ], + "config": { + "background": "#1b1a19", + "font": "'Segoe UI', 'Helvetica Neue', Arial, sans-serif", + "title": { + "font": "'Segoe UI', 'Helvetica Neue', Arial, sans-serif", + "fontSize": 12.5, + "fontWeight": 600, + "color": "#f3f2f1", + "anchor": "start", + "offset": 10, + "subtitleFont": "'Segoe UI', 'Helvetica Neue', Arial, sans-serif", + "subtitleFontSize": 9.5, + "subtitleColor": "#8a8886", + "subtitlePadding": 6 + }, + "view": { "stroke": null }, + "axis": { + "labelFont": "'Segoe UI', 'Helvetica Neue', Arial, sans-serif", + "labelFontSize": 9.5, + "labelColor": "#c8c6c4" + } + } +} diff --git a/site/src/playground/theme-lab-assets/gapminder-bubble.economist.json b/site/src/playground/theme-lab-assets/gapminder-bubble.economist.json new file mode 100644 index 00000000..a486d209 --- /dev/null +++ b/site/src/playground/theme-lab-assets/gapminder-bubble.economist.json @@ -0,0 +1,165 @@ +{ + "$schema": "https://vega-lite.github.io/schema/vega-lite/v5.json", + "__theme__": "economist", + "__design__": [ + "Log-scale ticks pinned to the decades and labelled as money ($1k / $10k / $100k). Flint's ',.12~g' format on a log axis prints whatever values d3 happens to pick, which is how a log axis ends up labelled 2,000 · 5,000 · 20,000 and stops looking logarithmic at all.", + "Both legends moved to a single row above the plot: continents as small filled circles, and the size key as three named population steps. A size ramp with no stated values is decoration — the reader can see that one bubble is bigger without being able to say by how much.", + "Size range narrowed from 9–361 to 12–260 and fill opacity dropped to 0.75 with a white hairline. At 15 bubbles the baseline's largest circles swallowed their neighbours; the halo is what keeps China and India separable where they overlap.", + "Palette switched from tableau10 to four house hues, bound to the continents by name so a continent keeps the same colour in every chart in the section instead of taking whatever tableau10 hands it by row position. The order the legend lists them in still follows the baseline.", + "Gridlines kept on both axes but pushed to #dfe6ea, because a scatter is read by position in two dimensions and removing the vertical rules would leave the log axis unusable.", + "Y axis given a bare 'years' unit and the x axis title deleted — the tick labels already carry dollar signs, so naming the axis 'GDP per capita' twice adds nothing.", + "Red masthead rule, 13.5pt bold headline over a grey deck, 9.5pt labels throughout." + ], + "background": "#ffffff", + "padding": { "left": 6, "top": 4, "right": 14, "bottom": 6 }, + "title": { + "text": "Money buys years, up to a point", + "subtitle": ["Life expectancy against GDP per capita, 2018; bubble area is population"] + }, + "spacing": 8, + "resolve": { "legend": { "color": "independent", "size": "independent" } }, + "vconcat": [ + { + "data": { "values": [{}] }, + "mark": { "type": "rect", "fill": "#e3120b", "stroke": null }, + "width": 26, + "height": 3, + "view": { "stroke": null } + }, + { + "width": 290, + "height": 200, + "data": { + "values": [ + { "Country": "Norway", "GDP per capita": 64800, "Life expectancy": 82.3, "Population (M)": 5.3, "Continent": "Europe" }, + { "Country": "United States", "GDP per capita": 62600, "Life expectancy": 78.6, "Population (M)": 327, "Continent": "Americas" }, + { "Country": "Japan", "GDP per capita": 39300, "Life expectancy": 84.2, "Population (M)": 127, "Continent": "Asia" }, + { "Country": "China", "GDP per capita": 16800, "Life expectancy": 76.7, "Population (M)": 1393, "Continent": "Asia" }, + { "Country": "India", "GDP per capita": 6900, "Life expectancy": 69.4, "Population (M)": 1353, "Continent": "Asia" }, + { "Country": "Nigeria", "GDP per capita": 5300, "Life expectancy": 54.3, "Population (M)": 196, "Continent": "Africa" }, + { "Country": "Brazil", "GDP per capita": 15600, "Life expectancy": 75.7, "Population (M)": 209, "Continent": "Americas" }, + { "Country": "Germany", "GDP per capita": 50900, "Life expectancy": 81, "Population (M)": 83, "Continent": "Europe" }, + { "Country": "Ethiopia", "GDP per capita": 2000, "Life expectancy": 66.2, "Population (M)": 109, "Continent": "Africa" }, + { "Country": "Russia", "GDP per capita": 25800, "Life expectancy": 72.4, "Population (M)": 145, "Continent": "Europe" }, + { "Country": "Mexico", "GDP per capita": 19800, "Life expectancy": 75, "Population (M)": 126, "Continent": "Americas" }, + { "Country": "Indonesia", "GDP per capita": 12400, "Life expectancy": 71.5, "Population (M)": 268, "Continent": "Asia" }, + { "Country": "Qatar", "GDP per capita": 116900, "Life expectancy": 80.1, "Population (M)": 2.8, "Continent": "Asia" }, + { "Country": "South Africa", "GDP per capita": 13000, "Life expectancy": 63.9, "Population (M)": 57, "Continent": "Africa" }, + { "Country": "Bangladesh", "GDP per capita": 4200, "Life expectancy": 72.3, "Population (M)": 161, "Continent": "Asia" } + ] + }, + "mark": { + "type": "circle", + "opacity": 0.75, + "stroke": "#ffffff", + "strokeWidth": 0.8 + }, + "encoding": { + "x": { + "field": "GDP per capita", + "type": "quantitative", + "title": null, + "scale": { "type": "log", "domain": [1500, 160000], "nice": false }, + "axis": { + "values": [2000, 5000, 10000, 20000, 50000, 100000], + "labelExpr": "datum.value >= 1000 ? '$' + (datum.value / 1000) + 'k' : '$' + datum.value", + "grid": true, + "gridColor": "#dfe6ea", + "labelPadding": 4 + } + }, + "y": { + "field": "Life expectancy", + "type": "quantitative", + "title": "years", + "scale": { "zero": false, "nice": false, "domain": [50, 88] }, + "axis": { + "tickCount": 4, + "format": ".0f", + "grid": true, + "gridColor": "#dfe6ea", + "labelPadding": 4, + "titleAngle": 0, + "titleAlign": "left", + "titleAnchor": "start", + "titleBaseline": "bottom", + "titleY": -8, + "titlePadding": 0 + } + }, + "size": { + "field": "Population (M)", + "type": "quantitative", + "title": "population, m", + "scale": { "type": "sqrt", "zero": true, "range": [12, 260] }, + "legend": { + "orient": "top", + "direction": "horizontal", + "values": [100, 500, 1400], + "format": ",.0f", + "symbolFillColor": "#b8c4cc", + "symbolStrokeColor": "#8fa0ab", + "titleFontSize": 9, + "titleFontWeight": 400, + "titleColor": "#54585a", + "labelFontSize": 9, + "offset": 4, + "padding": 0 + } + }, + "color": { + "field": "Continent", + "type": "nominal", + "title": null, + "scale": { + "domain": ["Europe", "Americas", "Asia", "Africa"], + "range": ["#379a8b", "#ebb434", "#006ba2", "#e3120b"] + }, + "legend": { + "orient": "top", + "direction": "horizontal", + "symbolType": "circle", + "symbolSize": 70, + "columnPadding": 8, + "labelFontSize": 9.5, + "offset": 4, + "padding": 0 + } + } + } + } + ], + "config": { + "background": "#ffffff", + "font": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "title": { + "font": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "fontSize": 13.5, + "fontWeight": 700, + "color": "#121317", + "anchor": "start", + "offset": 10, + "subtitleFont": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "subtitleFontSize": 10.5, + "subtitleColor": "#54585a", + "subtitlePadding": 6 + }, + "view": { "stroke": null }, + "axis": { + "labelFont": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "labelFontSize": 9.5, + "labelColor": "#54585a", + "titleFont": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "titleFontSize": 9.5, + "titleFontWeight": 400, + "titleColor": "#54585a", + "domain": false, + "ticks": false + }, + "legend": { + "labelFont": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "labelColor": "#54585a", + "titleFont": "'Helvetica Neue', Helvetica, Arial, sans-serif" + } + } +} diff --git a/site/src/playground/theme-lab-assets/gapminder-bubble.flint.json b/site/src/playground/theme-lab-assets/gapminder-bubble.flint.json new file mode 100644 index 00000000..0e850716 --- /dev/null +++ b/site/src/playground/theme-lab-assets/gapminder-bubble.flint.json @@ -0,0 +1,184 @@ +{ + "mark": "circle", + "encoding": { + "x": { + "field": "GDP per capita", + "type": "quantitative", + "scale": { + "type": "log" + }, + "axis": { + "gridColor": "#e8e8e8", + "gridOpacity": 0.5, + "format": ",.12~g" + } + }, + "y": { + "field": "Life expectancy", + "type": "quantitative", + "scale": { + "zero": false + }, + "axis": { + "format": ",.12~g" + } + }, + "size": { + "field": "Population (M)", + "type": "quantitative", + "scale": { + "type": "sqrt", + "zero": true, + "range": [ + 9, + 361 + ] + } + }, + "color": { + "field": "Continent", + "type": "nominal", + "sort": null, + "scale": { + "scheme": "tableau10" + } + } + }, + "config": { + "view": { + "continuousWidth": 309, + "continuousHeight": 253 + }, + "axisX": { + "labelLimit": 100, + "labelFontSize": 10, + "titleFontSize": 11 + }, + "axisY": { + "labelFontSize": 10, + "titleFontSize": 11 + }, + "legend": { + "labelFontSize": 10, + "titleFontSize": 11 + }, + "facet": { + "spacing": 25 + } + }, + "data": { + "values": [ + { + "Country": "Norway", + "GDP per capita": 64800, + "Life expectancy": 82.3, + "Population (M)": 5.3, + "Continent": "Europe" + }, + { + "Country": "United States", + "GDP per capita": 62600, + "Life expectancy": 78.6, + "Population (M)": 327, + "Continent": "Americas" + }, + { + "Country": "Japan", + "GDP per capita": 39300, + "Life expectancy": 84.2, + "Population (M)": 127, + "Continent": "Asia" + }, + { + "Country": "China", + "GDP per capita": 16800, + "Life expectancy": 76.7, + "Population (M)": 1393, + "Continent": "Asia" + }, + { + "Country": "India", + "GDP per capita": 6900, + "Life expectancy": 69.4, + "Population (M)": 1353, + "Continent": "Asia" + }, + { + "Country": "Nigeria", + "GDP per capita": 5300, + "Life expectancy": 54.3, + "Population (M)": 196, + "Continent": "Africa" + }, + { + "Country": "Brazil", + "GDP per capita": 15600, + "Life expectancy": 75.7, + "Population (M)": 209, + "Continent": "Americas" + }, + { + "Country": "Germany", + "GDP per capita": 50900, + "Life expectancy": 81, + "Population (M)": 83, + "Continent": "Europe" + }, + { + "Country": "Ethiopia", + "GDP per capita": 2000, + "Life expectancy": 66.2, + "Population (M)": 109, + "Continent": "Africa" + }, + { + "Country": "Russia", + "GDP per capita": 25800, + "Life expectancy": 72.4, + "Population (M)": 145, + "Continent": "Europe" + }, + { + "Country": "Mexico", + "GDP per capita": 19800, + "Life expectancy": 75, + "Population (M)": 126, + "Continent": "Americas" + }, + { + "Country": "Indonesia", + "GDP per capita": 12400, + "Life expectancy": 71.5, + "Population (M)": 268, + "Continent": "Asia" + }, + { + "Country": "Qatar", + "GDP per capita": 116900, + "Life expectancy": 80.1, + "Population (M)": 2.8, + "Continent": "Asia" + }, + { + "Country": "South Africa", + "GDP per capita": 13000, + "Life expectancy": 63.9, + "Population (M)": 57, + "Continent": "Africa" + }, + { + "Country": "Bangladesh", + "GDP per capita": 4200, + "Life expectancy": 72.3, + "Population (M)": 161, + "Continent": "Asia" + } + ] + }, + "title": { + "text": "Money buys years, up to a point", + "subtitle": [ + "Life expectancy against GDP per capita, 2018; bubble area is population" + ] + } +} diff --git a/site/src/playground/theme-lab-assets/gdp-bartable.flint.json b/site/src/playground/theme-lab-assets/gdp-bartable.flint.json new file mode 100644 index 00000000..6e80bc0a --- /dev/null +++ b/site/src/playground/theme-lab-assets/gdp-bartable.flint.json @@ -0,0 +1,284 @@ +{ + "spacing": 8, + "resolve": { + "scale": { + "y": "shared" + } + }, + "hconcat": [ + { + "data": { + "name": "__bt_displayTable" + }, + "width": 180, + "height": 230, + "title": { + "text": "Country", + "anchor": "start", + "offset": 6, + "fontSize": 12, + "fontWeight": "normal", + "color": "#999" + }, + "mark": { + "type": "bar", + "height": { + "band": 0.842 + } + }, + "encoding": { + "y": { + "field": "Country", + "type": "nominal", + "sort": [ + "United States", + "China", + "Germany", + "Japan", + "India", + "UK", + "France", + "Brazil" + ], + "axis": { + "title": null, + "domain": false, + "ticks": false, + "labelFontSize": 13, + "labelAlign": "left", + "labelPadding": 105, + "labelLimit": 105 + } + }, + "x": { + "field": "GDP ($T)", + "type": "quantitative", + "axis": null, + "scale": { + "nice": false + } + }, + "color": { + "field": "GDP ($T)", + "type": "quantitative", + "legend": null, + "scale": { + "range": [ + "#cdebd3", + "#41a25f" + ] + } + } + } + }, + { + "data": { + "name": "__bt_displayTable" + }, + "width": 66, + "height": 230, + "transform": [ + { + "aggregate": [ + { + "op": "sum", + "field": "GDP ($T)", + "as": "__bt_val" + }, + { + "op": "min", + "field": "__bt_sort", + "as": "__bt_sort" + }, + { + "op": "max", + "field": "__bt_others_num", + "as": "__bt_others_num" + } + ], + "groupby": [ + "Country" + ] + } + ], + "title": { + "text": "GDP ($T)", + "anchor": "end", + "offset": 6, + "limit": 62, + "fontSize": 12, + "fontWeight": "normal", + "color": "#999" + }, + "mark": { + "type": "text", + "align": "right", + "baseline": "middle", + "fontSize": 12 + }, + "encoding": { + "y": { + "field": "Country", + "type": "nominal", + "sort": [ + "United States", + "China", + "Germany", + "Japan", + "India", + "UK", + "France", + "Brazil" + ], + "axis": null + }, + "x": { + "datum": 1, + "axis": null, + "scale": { + "type": "linear", + "domain": [ + 0, + 1 + ] + } + }, + "text": { + "field": "__bt_val", + "type": "quantitative" + }, + "color": { + "value": "#666" + } + } + } + ], + "config": { + "view": { + "continuousWidth": 340, + "continuousHeight": 230, + "stroke": null + }, + "axisX": { + "labelLimit": 100, + "labelFontSize": 10, + "titleFontSize": 11 + }, + "axisY": { + "labelFontSize": 10, + "titleFontSize": 11 + }, + "legend": { + "labelFontSize": 10, + "titleFontSize": 11 + }, + "facet": { + "spacing": 23 + } + }, + "datasets": { + "__bt_displayTable": [ + { + "Country": "United States", + "GDP ($T)": 27.4, + "__bt_sort": 0, + "__bt_others": false, + "__bt_others_num": 0 + }, + { + "Country": "China", + "GDP ($T)": 17.8, + "__bt_sort": 1, + "__bt_others": false, + "__bt_others_num": 0 + }, + { + "Country": "Germany", + "GDP ($T)": 4.5, + "__bt_sort": 2, + "__bt_others": false, + "__bt_others_num": 0 + }, + { + "Country": "Japan", + "GDP ($T)": 4.2, + "__bt_sort": 3, + "__bt_others": false, + "__bt_others_num": 0 + }, + { + "Country": "India", + "GDP ($T)": 3.9, + "__bt_sort": 4, + "__bt_others": false, + "__bt_others_num": 0 + }, + { + "Country": "UK", + "GDP ($T)": 3.3, + "__bt_sort": 5, + "__bt_others": false, + "__bt_others_num": 0 + }, + { + "Country": "France", + "GDP ($T)": 3, + "__bt_sort": 6, + "__bt_others": false, + "__bt_others_num": 0 + }, + { + "Country": "Brazil", + "GDP ($T)": 2.2, + "__bt_sort": 7, + "__bt_others": false, + "__bt_others_num": 0 + } + ] + }, + "height": { + "step": 27 + }, + "data": { + "values": [ + { + "Country": "United States", + "GDP ($T)": 27.4 + }, + { + "Country": "China", + "GDP ($T)": 17.8 + }, + { + "Country": "Germany", + "GDP ($T)": 4.5 + }, + { + "Country": "Japan", + "GDP ($T)": 4.2 + }, + { + "Country": "India", + "GDP ($T)": 3.9 + }, + { + "Country": "UK", + "GDP ($T)": 3.3 + }, + { + "Country": "France", + "GDP ($T)": 3 + }, + { + "Country": "Brazil", + "GDP ($T)": 2.2 + } + ] + }, + "title": { + "text": "America and China lap the field", + "subtitle": [ + "Gross domestic product, 2023, trillion US dollars" + ] + } +} diff --git a/site/src/playground/theme-lab-assets/gdp-bartable.mckinsey.json b/site/src/playground/theme-lab-assets/gdp-bartable.mckinsey.json new file mode 100644 index 00000000..d4224b9f --- /dev/null +++ b/site/src/playground/theme-lab-assets/gdp-bartable.mckinsey.json @@ -0,0 +1,95 @@ +{ + "$schema": "https://vega-lite.github.io/schema/vega-lite/v5.json", + "__theme__": "mckinsey", + "__design__": [ + "The two-panel hconcat collapsed into one plot. Flint builds a bar table as a 180px chart glued to a 66px text panel driven by a fake 'x: {datum: 1}' scale and a duplicated '__bt_displayTable'. The same result — label, bar, number — is one layered spec, and the number now sits at the end of the bar it belongs to instead of in a detached column the eye has to re-associate by row.", + "One measure, one colour: every bar in a flat brand blue #2251FF. Flint maps GDP to both bar length and a #cdebd3→#41a25f ramp — the same number encoded twice, which buys no information and implies a continuous scale the reader has no key for. Length already carries the quantity.", + "Value axis deleted; the numbers are on the marks. Nothing here needs traversal to a ruler.", + "The 105px labelPadding / 105px labelLimit hack removed. Flint pushes country names 105px right and then truncates them at exactly the same width, which is how 'United States' ends up at the edge of clipping. Ordinary right-aligned axis labels do the job.", + "Row order pinned in the spec rather than inherited from row position — the rendered sequence is the baseline's — and the x domain padded to 31 so the labels sit outside the bars." + ], + "background": "#ffffff", + "padding": { "left": 8, "top": 6, "right": 26, "bottom": 8 }, + "title": { + "text": "America and China lap the field", + "subtitle": ["Gross domestic product, 2023, trillion US dollars"] + }, + "width": 250, + "height": 200, + "data": { + "values": [ + { "Country": "United States", "GDP ($T)": 27.4 }, + { "Country": "China", "GDP ($T)": 17.8 }, + { "Country": "Germany", "GDP ($T)": 4.5 }, + { "Country": "Japan", "GDP ($T)": 4.2 }, + { "Country": "India", "GDP ($T)": 3.9 }, + { "Country": "UK", "GDP ($T)": 3.3 }, + { "Country": "France", "GDP ($T)": 3 }, + { "Country": "Brazil", "GDP ($T)": 2.2 } + ] + }, + "encoding": { + "y": { + "field": "Country", + "type": "nominal", + "sort": { "field": "GDP ($T)", "op": "max", "order": "descending" }, + "title": null, + "axis": { "domain": false, "ticks": false, "grid": false, "labelPadding": 6 } + }, + "x": { + "field": "GDP ($T)", + "type": "quantitative", + "title": null, + "scale": { "zero": true, "nice": false, "domain": [0, 31] }, + "axis": null + } + }, + "layer": [ + { + "mark": { "type": "bar", "height": { "band": 0.62 }, "color": "#2251ff" } + }, + { + "mark": { + "type": "text", + "align": "left", + "baseline": "middle", + "dx": 5, + "font": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "fontSize": 10, + "fontWeight": 600, + "color": "#051c2c" + }, + "encoding": { + "text": { "field": "GDP ($T)", "type": "quantitative", "format": ".1f" } + } + } + ], + "config": { + "background": "#ffffff", + "font": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "title": { + "font": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "fontSize": 14, + "fontWeight": 700, + "color": "#051c2c", + "anchor": "start", + "offset": 12, + "subtitleFont": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "subtitleFontSize": 10.5, + "subtitleColor": "#5c6b75", + "subtitlePadding": 6 + }, + "view": { "stroke": null }, + "axis": { + "labelFont": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "labelFontSize": 10, + "labelColor": "#051c2c", + "titleFont": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "titleFontSize": 9, + "titleColor": "#8fa0ab", + "grid": false, + "domain": false, + "ticks": false + } + } +} diff --git a/site/src/playground/theme-lab-assets/happiness.flint.json b/site/src/playground/theme-lab-assets/happiness.flint.json new file mode 100644 index 00000000..b05620c1 --- /dev/null +++ b/site/src/playground/theme-lab-assets/happiness.flint.json @@ -0,0 +1,116 @@ +{ + "mark": "circle", + "encoding": { + "x": { + "field": "GDP per capita", + "type": "quantitative", + "scale": { + "type": "log" + }, + "axis": { + "gridColor": "#e8e8e8", + "gridOpacity": 0.5, + "format": ",.12~g" + } + }, + "y": { + "field": "Happiness score", + "type": "quantitative", + "scale": { + "zero": false + }, + "axis": { + "format": ",.12~g" + } + } + }, + "config": { + "view": { + "continuousWidth": 312, + "continuousHeight": 251 + }, + "axisX": { + "labelLimit": 100, + "labelFontSize": 10, + "titleFontSize": 11 + }, + "axisY": { + "labelFontSize": 10, + "titleFontSize": 11 + }, + "legend": { + "labelFontSize": 10, + "titleFontSize": 11 + }, + "facet": { + "spacing": 25 + } + }, + "data": { + "values": [ + { + "Country": "Finland", + "GDP per capita": 54000, + "Happiness score": 7.8 + }, + { + "Country": "Denmark", + "GDP per capita": 68000, + "Happiness score": 7.6 + }, + { + "Country": "United States", + "GDP per capita": 76000, + "Happiness score": 6.9 + }, + { + "Country": "Germany", + "GDP per capita": 51000, + "Happiness score": 7 + }, + { + "Country": "Japan", + "GDP per capita": 34000, + "Happiness score": 6.1 + }, + { + "Country": "Brazil", + "GDP per capita": 9000, + "Happiness score": 6.1 + }, + { + "Country": "China", + "GDP per capita": 12800, + "Happiness score": 5.8 + }, + { + "Country": "Mexico", + "GDP per capita": 11000, + "Happiness score": 6.3 + }, + { + "Country": "India", + "GDP per capita": 2400, + "Happiness score": 4 + }, + { + "Country": "Nigeria", + "GDP per capita": 2200, + "Happiness score": 4.9 + }, + { + "Country": "Kenya", + "GDP per capita": 2100, + "Happiness score": 4.5 + }, + { + "Country": "Costa Rica", + "GDP per capita": 12500, + "Happiness score": 7.1 + } + ] + }, + "title": { + "text": "World Happiness vs income per capita (2023)" + } +} diff --git a/site/src/playground/theme-lab-assets/internet-users.flint.json b/site/src/playground/theme-lab-assets/internet-users.flint.json new file mode 100644 index 00000000..7be9c0cd --- /dev/null +++ b/site/src/playground/theme-lab-assets/internet-users.flint.json @@ -0,0 +1,81 @@ +{ + "mark": "area", + "encoding": { + "x": { + "field": "Year", + "type": "temporal" + }, + "y": { + "field": "Internet users (%)", + "type": "quantitative", + "stack": null, + "scale": { + "zero": true + }, + "axis": { + "format": ",.12~g" + } + } + }, + "config": { + "view": { + "continuousWidth": 315, + "continuousHeight": 248 + }, + "axisX": { + "labelLimit": 100, + "labelFontSize": 10, + "titleFontSize": 11 + }, + "axisY": { + "labelFontSize": 10, + "titleFontSize": 11 + }, + "legend": { + "labelFontSize": 10, + "titleFontSize": 11 + }, + "facet": { + "spacing": 25 + } + }, + "data": { + "values": [ + { + "Year": "1995", + "Internet users (%)": 1 + }, + { + "Year": "2000", + "Internet users (%)": 7 + }, + { + "Year": "2005", + "Internet users (%)": 16 + }, + { + "Year": "2010", + "Internet users (%)": 29 + }, + { + "Year": "2015", + "Internet users (%)": 43 + }, + { + "Year": "2018", + "Internet users (%)": 51 + }, + { + "Year": "2020", + "Internet users (%)": 60 + }, + { + "Year": "2023", + "Internet users (%)": 67 + } + ] + }, + "title": { + "text": "Share of the world online, 1995–2023 (%)" + } +} diff --git a/site/src/playground/theme-lab-assets/iris-strip.flint.json b/site/src/playground/theme-lab-assets/iris-strip.flint.json new file mode 100644 index 00000000..807fc4bc --- /dev/null +++ b/site/src/playground/theme-lab-assets/iris-strip.flint.json @@ -0,0 +1,193 @@ +{ + "mark": { + "type": "circle", + "opacity": 0.6, + "size": 100 + }, + "encoding": { + "x": { + "field": "Species", + "type": "nominal", + "sort": null + }, + "y": { + "field": "Petal length (cm)", + "type": "quantitative", + "scale": { + "zero": false + }, + "axis": { + "format": ",.12~g" + } + }, + "xOffset": { + "field": "__jitter", + "type": "quantitative", + "axis": null, + "scale": { + "domain": [ + -10, + 10 + ] + } + } + }, + "width": { + "step": 57 + }, + "transform": [ + { + "calculate": "-6 + random() * 12", + "as": "__jitter" + } + ], + "config": { + "view": { + "continuousWidth": 340, + "continuousHeight": 230 + }, + "axisX": { + "labelLimit": 100, + "labelFontSize": 10, + "titleFontSize": 11 + }, + "axisY": { + "labelFontSize": 10, + "titleFontSize": 11 + }, + "legend": { + "labelFontSize": 10, + "titleFontSize": 11 + }, + "facet": { + "spacing": 23 + } + }, + "data": { + "values": [ + { + "Species": "Setosa", + "Petal length (cm)": 1.4 + }, + { + "Species": "Setosa", + "Petal length (cm)": 1.4 + }, + { + "Species": "Setosa", + "Petal length (cm)": 1.3 + }, + { + "Species": "Setosa", + "Petal length (cm)": 1.5 + }, + { + "Species": "Setosa", + "Petal length (cm)": 1.4 + }, + { + "Species": "Setosa", + "Petal length (cm)": 1.7 + }, + { + "Species": "Setosa", + "Petal length (cm)": 1.4 + }, + { + "Species": "Setosa", + "Petal length (cm)": 1.5 + }, + { + "Species": "Setosa", + "Petal length (cm)": 1.5 + }, + { + "Species": "Setosa", + "Petal length (cm)": 1.6 + }, + { + "Species": "Versicolor", + "Petal length (cm)": 4.7 + }, + { + "Species": "Versicolor", + "Petal length (cm)": 4.5 + }, + { + "Species": "Versicolor", + "Petal length (cm)": 4.9 + }, + { + "Species": "Versicolor", + "Petal length (cm)": 4 + }, + { + "Species": "Versicolor", + "Petal length (cm)": 4.6 + }, + { + "Species": "Versicolor", + "Petal length (cm)": 4.5 + }, + { + "Species": "Versicolor", + "Petal length (cm)": 4.7 + }, + { + "Species": "Versicolor", + "Petal length (cm)": 3.3 + }, + { + "Species": "Versicolor", + "Petal length (cm)": 4.6 + }, + { + "Species": "Versicolor", + "Petal length (cm)": 3.9 + }, + { + "Species": "Virginica", + "Petal length (cm)": 6 + }, + { + "Species": "Virginica", + "Petal length (cm)": 5.1 + }, + { + "Species": "Virginica", + "Petal length (cm)": 5.9 + }, + { + "Species": "Virginica", + "Petal length (cm)": 5.6 + }, + { + "Species": "Virginica", + "Petal length (cm)": 5.8 + }, + { + "Species": "Virginica", + "Petal length (cm)": 6.6 + }, + { + "Species": "Virginica", + "Petal length (cm)": 4.5 + }, + { + "Species": "Virginica", + "Petal length (cm)": 6.3 + }, + { + "Species": "Virginica", + "Petal length (cm)": 5.8 + }, + { + "Species": "Virginica", + "Petal length (cm)": 6.1 + } + ] + }, + "title": { + "text": "Iris petal length by species" + } +} diff --git a/site/src/playground/theme-lab-assets/keeling.flint.json b/site/src/playground/theme-lab-assets/keeling.flint.json new file mode 100644 index 00000000..d77cc94f --- /dev/null +++ b/site/src/playground/theme-lab-assets/keeling.flint.json @@ -0,0 +1,107 @@ +{ + "mark": "line", + "encoding": { + "x": { + "field": "Year", + "type": "temporal" + }, + "y": { + "field": "CO₂ (ppm)", + "type": "quantitative", + "scale": { + "zero": true + }, + "axis": { + "format": ",.12~g" + } + } + }, + "config": { + "view": { + "continuousWidth": 308, + "continuousHeight": 254 + }, + "axisX": { + "labelLimit": 100, + "labelFontSize": 10, + "titleFontSize": 11 + }, + "axisY": { + "labelFontSize": 10, + "titleFontSize": 11 + }, + "legend": { + "labelFontSize": 10, + "titleFontSize": 11 + }, + "facet": { + "spacing": 25 + } + }, + "data": { + "values": [ + { + "Year": "1959", + "CO₂ (ppm)": 315.98 + }, + { + "Year": "1965", + "CO₂ (ppm)": 320.04 + }, + { + "Year": "1970", + "CO₂ (ppm)": 325.68 + }, + { + "Year": "1975", + "CO₂ (ppm)": 331.11 + }, + { + "Year": "1980", + "CO₂ (ppm)": 338.8 + }, + { + "Year": "1985", + "CO₂ (ppm)": 346.12 + }, + { + "Year": "1990", + "CO₂ (ppm)": 354.45 + }, + { + "Year": "1995", + "CO₂ (ppm)": 360.82 + }, + { + "Year": "2000", + "CO₂ (ppm)": 369.71 + }, + { + "Year": "2005", + "CO₂ (ppm)": 379.98 + }, + { + "Year": "2010", + "CO₂ (ppm)": 389.9 + }, + { + "Year": "2015", + "CO₂ (ppm)": 400.83 + }, + { + "Year": "2020", + "CO₂ (ppm)": 414.24 + }, + { + "Year": "2023", + "CO₂ (ppm)": 421.08 + } + ] + }, + "title": { + "text": "Keeling Curve", + "subtitle": [ + "Atmospheric CO₂ at Mauna Loa, annual mean, parts per million" + ] + } +} diff --git a/site/src/playground/theme-lab-assets/keeling.nyt.json b/site/src/playground/theme-lab-assets/keeling.nyt.json new file mode 100644 index 00000000..15d29b34 --- /dev/null +++ b/site/src/playground/theme-lab-assets/keeling.nyt.json @@ -0,0 +1,107 @@ +{ + "$schema": "https://vega-lite.github.io/schema/vega-lite/v5.json", + "__theme__": "nyt", + "__design__": [ + "Title typography carries the hierarchy instead of a caption: serif (Georgia) at 15pt, flush left, with the unit split out as a sans deck. Same words as Flint's title, different weight structure.", + "Zero baseline dropped (scale.zero true → false). Flint is right to default to zero for a quantity, but a ppm record needs the top 100 ppm of range to read as a curve.", + "Frame removed: view.stroke null, horizontal gridlines only (#e4e4e4), and the y-axis domain line deleted. The x-axis keeps a black baseline with outward ticks so the time direction stays anchored.", + "Y-axis title rotated flat (titleAngle 0) and parked above the axis as a bare unit; x-axis title deleted, because a year axis labels itself.", + "Tick count cut from Flint's default to 4 on y and flush-aligned end labels on x — fewer, larger numbers rather than an even grid.", + "One editorial accent (#c2352b) at 2.5px with round caps, replacing Flint's default blue hairline." + ], + "background": "#ffffff", + "padding": { "left": 8, "top": 6, "right": 34, "bottom": 6 }, + "width": 300, + "height": 190, + "title": { + "text": "Keeling Curve", + "subtitle": ["Atmospheric CO₂ at Mauna Loa, annual mean, parts per million"] + }, + "data": { + "values": [ + { "Year": "1959", "CO₂ (ppm)": 315.98 }, + { "Year": "1965", "CO₂ (ppm)": 320.04 }, + { "Year": "1970", "CO₂ (ppm)": 325.68 }, + { "Year": "1975", "CO₂ (ppm)": 331.11 }, + { "Year": "1980", "CO₂ (ppm)": 338.8 }, + { "Year": "1985", "CO₂ (ppm)": 346.12 }, + { "Year": "1990", "CO₂ (ppm)": 354.45 }, + { "Year": "1995", "CO₂ (ppm)": 360.82 }, + { "Year": "2000", "CO₂ (ppm)": 369.71 }, + { "Year": "2005", "CO₂ (ppm)": 379.98 }, + { "Year": "2010", "CO₂ (ppm)": 389.9 }, + { "Year": "2015", "CO₂ (ppm)": 400.83 }, + { "Year": "2020", "CO₂ (ppm)": 414.24 }, + { "Year": "2023", "CO₂ (ppm)": 421.08 } + ] + }, + "encoding": { + "x": { + "field": "Year", + "type": "temporal", + "title": null, + "axis": { "format": "%Y", "tickCount": 5 } + }, + "y": { + "field": "CO₂ (ppm)", + "type": "quantitative", + "title": "ppm", + "scale": { "zero": false, "nice": true }, + "axis": { "tickCount": 4, "format": ",.0f" } + } + }, + "mark": { "type": "line", "color": "#c2352b", "strokeWidth": 2.5, "strokeCap": "round" }, + "config": { + "background": "#ffffff", + "font": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "title": { + "font": "Georgia, 'Times New Roman', Times, serif", + "fontSize": 15, + "fontWeight": 700, + "color": "#121212", + "anchor": "start", + "offset": 14, + "subtitleFont": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "subtitleFontSize": 11, + "subtitleColor": "#6b6b6b", + "subtitlePadding": 7 + }, + "view": { "stroke": null }, + "axis": { + "labelFont": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "labelFontSize": 10, + "labelColor": "#6b6b6b", + "titleFont": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "titleFontSize": 10, + "titleFontWeight": 400, + "titleColor": "#8e8e8e", + "domain": false, + "ticks": false, + "grid": false + }, + "axisY": { + "grid": true, + "gridColor": "#e4e4e4", + "gridWidth": 1, + "domain": false, + "ticks": false, + "labelPadding": 4, + "titleAngle": 0, + "titleAlign": "left", + "titleAnchor": "start", + "titleBaseline": "bottom", + "titleY": -8, + "titlePadding": 0 + }, + "axisX": { + "grid": false, + "domain": true, + "domainColor": "#121212", + "domainWidth": 1, + "ticks": true, + "tickColor": "#121212", + "tickSize": 4, + "labelFlush": true + } + } +} diff --git a/site/src/playground/theme-lab-assets/kpi-sparkline.flint.json b/site/src/playground/theme-lab-assets/kpi-sparkline.flint.json new file mode 100644 index 00000000..65c9b2a1 --- /dev/null +++ b/site/src/playground/theme-lab-assets/kpi-sparkline.flint.json @@ -0,0 +1,635 @@ +{ + "hconcat": [ + { + "data": { + "values": [ + { + "Metric": "Revenue ($k)" + }, + { + "Metric": "Active users (k)" + }, + { + "Metric": "Churn (%)" + } + ] + }, + "facet": { + "row": { + "field": "Metric", + "type": "nominal", + "sort": [ + "Revenue ($k)", + "Active users (k)", + "Churn (%)" + ], + "header": null + } + }, + "spec": { + "width": 116, + "height": 64, + "mark": { + "type": "text", + "align": "left", + "baseline": "middle", + "fontSize": 11 + }, + "encoding": { + "y": { + "value": 32 + }, + "x": { + "value": 0 + }, + "text": { + "field": "Metric", + "type": "nominal" + } + } + }, + "title": { + "text": "Metric", + "anchor": "start", + "fontSize": 11, + "fontWeight": "normal", + "color": "#999", + "offset": 6 + } + }, + { + "data": { + "values": [ + { + "Month": "Jan", + "Metric": "Revenue ($k)", + "Value": 120 + }, + { + "Month": "Feb", + "Metric": "Revenue ($k)", + "Value": 125 + }, + { + "Month": "Mar", + "Metric": "Revenue ($k)", + "Value": 130 + }, + { + "Month": "Apr", + "Metric": "Revenue ($k)", + "Value": 128 + }, + { + "Month": "May", + "Metric": "Revenue ($k)", + "Value": 140 + }, + { + "Month": "Jun", + "Metric": "Revenue ($k)", + "Value": 145 + }, + { + "Month": "Jul", + "Metric": "Revenue ($k)", + "Value": 150 + }, + { + "Month": "Aug", + "Metric": "Revenue ($k)", + "Value": 148 + }, + { + "Month": "Sep", + "Metric": "Revenue ($k)", + "Value": 160 + }, + { + "Month": "Oct", + "Metric": "Revenue ($k)", + "Value": 165 + }, + { + "Month": "Nov", + "Metric": "Revenue ($k)", + "Value": 170 + }, + { + "Month": "Dec", + "Metric": "Revenue ($k)", + "Value": 180 + }, + { + "Month": "Jan", + "Metric": "Active users (k)", + "Value": 40 + }, + { + "Month": "Feb", + "Metric": "Active users (k)", + "Value": 42 + }, + { + "Month": "Mar", + "Metric": "Active users (k)", + "Value": 45 + }, + { + "Month": "Apr", + "Metric": "Active users (k)", + "Value": 47 + }, + { + "Month": "May", + "Metric": "Active users (k)", + "Value": 50 + }, + { + "Month": "Jun", + "Metric": "Active users (k)", + "Value": 52 + }, + { + "Month": "Jul", + "Metric": "Active users (k)", + "Value": 55 + }, + { + "Month": "Aug", + "Metric": "Active users (k)", + "Value": 58 + }, + { + "Month": "Sep", + "Metric": "Active users (k)", + "Value": 60 + }, + { + "Month": "Oct", + "Metric": "Active users (k)", + "Value": 63 + }, + { + "Month": "Nov", + "Metric": "Active users (k)", + "Value": 66 + }, + { + "Month": "Dec", + "Metric": "Active users (k)", + "Value": 70 + }, + { + "Month": "Jan", + "Metric": "Churn (%)", + "Value": 5.2 + }, + { + "Month": "Feb", + "Metric": "Churn (%)", + "Value": 5 + }, + { + "Month": "Mar", + "Metric": "Churn (%)", + "Value": 4.8 + }, + { + "Month": "Apr", + "Metric": "Churn (%)", + "Value": 4.9 + }, + { + "Month": "May", + "Metric": "Churn (%)", + "Value": 4.6 + }, + { + "Month": "Jun", + "Metric": "Churn (%)", + "Value": 4.5 + }, + { + "Month": "Jul", + "Metric": "Churn (%)", + "Value": 4.3 + }, + { + "Month": "Aug", + "Metric": "Churn (%)", + "Value": 4.4 + }, + { + "Month": "Sep", + "Metric": "Churn (%)", + "Value": 4.1 + }, + { + "Month": "Oct", + "Metric": "Churn (%)", + "Value": 4 + }, + { + "Month": "Nov", + "Metric": "Churn (%)", + "Value": 3.9 + }, + { + "Month": "Dec", + "Metric": "Churn (%)", + "Value": 3.8 + } + ] + }, + "facet": { + "row": { + "field": "Metric", + "type": "nominal", + "sort": [ + "Revenue ($k)", + "Active users (k)", + "Churn (%)" + ], + "header": null + } + }, + "spec": { + "width": 154, + "height": 64, + "layer": [ + { + "mark": { + "type": "line", + "strokeWidth": 1.5 + }, + "encoding": { + "x": { + "field": "Month", + "type": "nominal", + "sort": [ + "Jan", + "Feb", + "Mar", + "Apr", + "May", + "Jun", + "Jul", + "Aug", + "Sep", + "Oct", + "Nov", + "Dec" + ], + "axis": null + }, + "y": { + "field": "Value", + "type": "quantitative", + "axis": null, + "scale": { + "range": [ + 62, + 2 + ] + } + }, + "color": { + "field": "Metric", + "type": "nominal", + "legend": null + } + } + }, + { + "mark": { + "type": "rule", + "strokeDash": [ + 3, + 2 + ], + "stroke": "#9a9a9a", + "strokeWidth": 1, + "opacity": 0.7 + }, + "encoding": { + "y": { + "field": "Value", + "aggregate": "mean", + "type": "quantitative", + "axis": null + } + } + } + ] + }, + "resolve": { + "scale": { + "y": "independent" + } + }, + "title": { + "text": "Value", + "anchor": "middle", + "fontSize": 11, + "fontWeight": "normal", + "color": "#999", + "offset": 6 + } + }, + { + "data": { + "values": [ + { + "Metric": "Revenue ($k)", + "flintSparkAvg": 146.75 + }, + { + "Metric": "Active users (k)", + "flintSparkAvg": 54 + }, + { + "Metric": "Churn (%)", + "flintSparkAvg": 4.458333333333333 + } + ] + }, + "facet": { + "row": { + "field": "Metric", + "type": "nominal", + "sort": [ + "Revenue ($k)", + "Active users (k)", + "Churn (%)" + ], + "header": null + } + }, + "spec": { + "width": 54, + "height": 64, + "mark": { + "type": "text", + "align": "right", + "baseline": "middle", + "fontSize": 11, + "fontWeight": 600 + }, + "encoding": { + "y": { + "value": 32 + }, + "x": { + "value": 54 + }, + "text": { + "field": "flintSparkAvg", + "type": "quantitative", + "format": ".3~s" + }, + "color": { + "field": "Metric", + "type": "nominal", + "legend": null + } + } + }, + "title": { + "text": "Average", + "anchor": "end", + "fontSize": 11, + "fontWeight": "normal", + "color": "#999", + "offset": 6 + } + } + ], + "spacing": 8, + "resolve": { + "scale": { + "y": "independent", + "color": "shared" + } + }, + "config": { + "view": { + "continuousWidth": 250, + "continuousHeight": 83, + "stroke": null + }, + "axisX": { + "labelLimit": 100, + "labelFontSize": 8, + "titleFontSize": 11, + "titleFontWeight": "normal", + "titleColor": "#666" + }, + "axisY": { + "labelFontSize": 8, + "titleFontSize": 11, + "titleFontWeight": "normal", + "titleColor": "#666" + }, + "legend": { + "labelFontSize": 9, + "titleFontSize": 9 + }, + "facet": { + "spacing": 8 + } + }, + "width": { + "step": 21 + }, + "data": { + "values": [ + { + "Month": "Jan", + "Metric": "Revenue ($k)", + "Value": 120 + }, + { + "Month": "Feb", + "Metric": "Revenue ($k)", + "Value": 125 + }, + { + "Month": "Mar", + "Metric": "Revenue ($k)", + "Value": 130 + }, + { + "Month": "Apr", + "Metric": "Revenue ($k)", + "Value": 128 + }, + { + "Month": "May", + "Metric": "Revenue ($k)", + "Value": 140 + }, + { + "Month": "Jun", + "Metric": "Revenue ($k)", + "Value": 145 + }, + { + "Month": "Jul", + "Metric": "Revenue ($k)", + "Value": 150 + }, + { + "Month": "Aug", + "Metric": "Revenue ($k)", + "Value": 148 + }, + { + "Month": "Sep", + "Metric": "Revenue ($k)", + "Value": 160 + }, + { + "Month": "Oct", + "Metric": "Revenue ($k)", + "Value": 165 + }, + { + "Month": "Nov", + "Metric": "Revenue ($k)", + "Value": 170 + }, + { + "Month": "Dec", + "Metric": "Revenue ($k)", + "Value": 180 + }, + { + "Month": "Jan", + "Metric": "Active users (k)", + "Value": 40 + }, + { + "Month": "Feb", + "Metric": "Active users (k)", + "Value": 42 + }, + { + "Month": "Mar", + "Metric": "Active users (k)", + "Value": 45 + }, + { + "Month": "Apr", + "Metric": "Active users (k)", + "Value": 47 + }, + { + "Month": "May", + "Metric": "Active users (k)", + "Value": 50 + }, + { + "Month": "Jun", + "Metric": "Active users (k)", + "Value": 52 + }, + { + "Month": "Jul", + "Metric": "Active users (k)", + "Value": 55 + }, + { + "Month": "Aug", + "Metric": "Active users (k)", + "Value": 58 + }, + { + "Month": "Sep", + "Metric": "Active users (k)", + "Value": 60 + }, + { + "Month": "Oct", + "Metric": "Active users (k)", + "Value": 63 + }, + { + "Month": "Nov", + "Metric": "Active users (k)", + "Value": 66 + }, + { + "Month": "Dec", + "Metric": "Active users (k)", + "Value": 70 + }, + { + "Month": "Jan", + "Metric": "Churn (%)", + "Value": 5.2 + }, + { + "Month": "Feb", + "Metric": "Churn (%)", + "Value": 5 + }, + { + "Month": "Mar", + "Metric": "Churn (%)", + "Value": 4.8 + }, + { + "Month": "Apr", + "Metric": "Churn (%)", + "Value": 4.9 + }, + { + "Month": "May", + "Metric": "Churn (%)", + "Value": 4.6 + }, + { + "Month": "Jun", + "Metric": "Churn (%)", + "Value": 4.5 + }, + { + "Month": "Jul", + "Metric": "Churn (%)", + "Value": 4.3 + }, + { + "Month": "Aug", + "Metric": "Churn (%)", + "Value": 4.4 + }, + { + "Month": "Sep", + "Metric": "Churn (%)", + "Value": 4.1 + }, + { + "Month": "Oct", + "Metric": "Churn (%)", + "Value": 4 + }, + { + "Month": "Nov", + "Metric": "Churn (%)", + "Value": 3.9 + }, + { + "Month": "Dec", + "Metric": "Churn (%)", + "Value": 3.8 + } + ] + }, + "title": { + "text": "Monthly KPIs", + "subtitle": [ + "Twelve-month trend and latest value" + ] + } +} diff --git a/site/src/playground/theme-lab-assets/kpi-sparkline.powerbi.json b/site/src/playground/theme-lab-assets/kpi-sparkline.powerbi.json new file mode 100644 index 00000000..9bcac33a --- /dev/null +++ b/site/src/playground/theme-lab-assets/kpi-sparkline.powerbi.json @@ -0,0 +1,152 @@ +{ + "$schema": "https://vega-lite.github.io/schema/vega-lite/v5.json", + "__theme__": "powerbi", + "__design__": [ + "Whole tile inverted to the #1B1A19 dashboard canvas so the three rows sit inside a report page rather than punching a white hole in it.", + "Rows given independent y scales. Flint shares one scale across all three metrics, which flattens revenue in the 120–180 range against churn at 3.8–5.2 into a single unreadable band; a sparkline row is a shape, not a magnitude comparison.", + "Colour freed from metric identity. The row label already names the metric, so the hue no longer has to; all three lines take Power BI green #22B14C because all three are currently moving in the desired direction — revenue and users up, churn down.", + "Row header moved from a rotated left-hand axis title to a flush-left label above each sparkline, which is how a KPI strip is laid out.", + "Current value printed at the right of each row, taken from the last point of the same series — the number a dashboard reader wants first, which the baseline made them read off a shared axis.", + "All axes, gridlines and ticks deleted. A sparkline has no axis by definition; the baseline's month labels and value ticks were consuming more pixels than the line.", + "Line weight 1.6px with a round join, plus a filled endpoint marker so the eye lands on the current period; row height cut to 34px so all three fit in one tile." + ], + "background": "#1b1a19", + "padding": { "left": 6, "top": 4, "right": 8, "bottom": 4 }, + "title": { + "text": "Monthly KPIs", + "subtitle": ["Twelve-month trend and latest value"] + }, + "data": { + "values": [ + { "Month": "Jan", "Metric": "Revenue ($k)", "Value": 120 }, + { "Month": "Feb", "Metric": "Revenue ($k)", "Value": 125 }, + { "Month": "Mar", "Metric": "Revenue ($k)", "Value": 130 }, + { "Month": "Apr", "Metric": "Revenue ($k)", "Value": 128 }, + { "Month": "May", "Metric": "Revenue ($k)", "Value": 140 }, + { "Month": "Jun", "Metric": "Revenue ($k)", "Value": 145 }, + { "Month": "Jul", "Metric": "Revenue ($k)", "Value": 150 }, + { "Month": "Aug", "Metric": "Revenue ($k)", "Value": 148 }, + { "Month": "Sep", "Metric": "Revenue ($k)", "Value": 160 }, + { "Month": "Oct", "Metric": "Revenue ($k)", "Value": 165 }, + { "Month": "Nov", "Metric": "Revenue ($k)", "Value": 170 }, + { "Month": "Dec", "Metric": "Revenue ($k)", "Value": 180 }, + { "Month": "Jan", "Metric": "Active users (k)", "Value": 40 }, + { "Month": "Feb", "Metric": "Active users (k)", "Value": 42 }, + { "Month": "Mar", "Metric": "Active users (k)", "Value": 45 }, + { "Month": "Apr", "Metric": "Active users (k)", "Value": 47 }, + { "Month": "May", "Metric": "Active users (k)", "Value": 50 }, + { "Month": "Jun", "Metric": "Active users (k)", "Value": 52 }, + { "Month": "Jul", "Metric": "Active users (k)", "Value": 55 }, + { "Month": "Aug", "Metric": "Active users (k)", "Value": 58 }, + { "Month": "Sep", "Metric": "Active users (k)", "Value": 60 }, + { "Month": "Oct", "Metric": "Active users (k)", "Value": 63 }, + { "Month": "Nov", "Metric": "Active users (k)", "Value": 66 }, + { "Month": "Dec", "Metric": "Active users (k)", "Value": 70 }, + { "Month": "Jan", "Metric": "Churn (%)", "Value": 5.2 }, + { "Month": "Feb", "Metric": "Churn (%)", "Value": 5.0 }, + { "Month": "Mar", "Metric": "Churn (%)", "Value": 4.8 }, + { "Month": "Apr", "Metric": "Churn (%)", "Value": 4.9 }, + { "Month": "May", "Metric": "Churn (%)", "Value": 4.6 }, + { "Month": "Jun", "Metric": "Churn (%)", "Value": 4.5 }, + { "Month": "Jul", "Metric": "Churn (%)", "Value": 4.3 }, + { "Month": "Aug", "Metric": "Churn (%)", "Value": 4.4 }, + { "Month": "Sep", "Metric": "Churn (%)", "Value": 4.1 }, + { "Month": "Oct", "Metric": "Churn (%)", "Value": 4.0 }, + { "Month": "Nov", "Metric": "Churn (%)", "Value": 3.9 }, + { "Month": "Dec", "Metric": "Churn (%)", "Value": 3.8 } + ] + }, + "facet": { + "row": { + "field": "Metric", + "type": "nominal", + "sort": ["Revenue ($k)", "Active users (k)", "Churn (%)"], + "title": null, + "header": { + "labelAngle": 0, + "labelAlign": "left", + "labelAnchor": "start", + "labelOrient": "top", + "labelFontSize": 10, + "labelFontWeight": 600, + "labelColor": "#f3f2f1", + "labelPadding": 0 + } + } + }, + "spacing": 14, + "resolve": { "scale": { "y": "independent" } }, + "spec": { + "width": 250, + "height": 34, + "encoding": { + "x": { + "field": "Month", + "type": "ordinal", + "sort": ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"], + "title": null, + "axis": null + }, + "y": { + "field": "Value", + "type": "quantitative", + "title": null, + "axis": null + } + }, + "layer": [ + { + "mark": { + "type": "line", + "color": "#22b14c", + "strokeWidth": 1.6, + "strokeJoin": "round", + "strokeCap": "round" + } + }, + { + "transform": [{ "filter": "datum.Month === 'Dec'" }], + "mark": { + "type": "point", + "filled": true, + "size": 40, + "color": "#22b14c" + } + }, + { + "transform": [{ "filter": "datum.Month === 'Dec'" }], + "mark": { + "type": "text", + "align": "left", + "baseline": "middle", + "dx": 8, + "font": "'Segoe UI', 'Helvetica Neue', Arial, sans-serif", + "fontSize": 12, + "fontWeight": 600, + "color": "#f3f2f1" + }, + "encoding": { + "text": { "field": "Value", "type": "quantitative", "format": ".3~f" } + } + } + ] + }, + "config": { + "background": "#1b1a19", + "font": "'Segoe UI', 'Helvetica Neue', Arial, sans-serif", + "title": { + "font": "'Segoe UI', 'Helvetica Neue', Arial, sans-serif", + "fontSize": 12.5, + "fontWeight": 600, + "color": "#f3f2f1", + "anchor": "start", + "offset": 10, + "subtitleFont": "'Segoe UI', 'Helvetica Neue', Arial, sans-serif", + "subtitleFontSize": 9.5, + "subtitleColor": "#8a8886", + "subtitlePadding": 6 + }, + "view": { "stroke": null }, + "header": { "labelFont": "'Segoe UI', 'Helvetica Neue', Arial, sans-serif" } + } +} diff --git a/site/src/playground/theme-lab-assets/life-expectancy.economist.json b/site/src/playground/theme-lab-assets/life-expectancy.economist.json new file mode 100644 index 00000000..28c5c813 --- /dev/null +++ b/site/src/playground/theme-lab-assets/life-expectancy.economist.json @@ -0,0 +1,139 @@ +{ + "$schema": "https://vega-lite.github.io/schema/vega-lite/v5.json", + "__theme__": "economist", + "__design__": [ + "Value axis deleted outright and both endpoints printed instead. A slope chart is read as a set of gradients between two labelled columns; the axis exists only so the reader can convert a height back into a number, which the number itself does better.", + "Legend replaced by the country name at both ends of every line. Seven lines that cross cannot be traced back to a key, and repeating the name on the right is what makes a crossing survivable.", + "Colour reduced from set2 to two house hues carrying a rule, not an identity: Economist blue #006BA2 where life expectancy rose, house red #E3120B where it fell. The rule is computed from the same two numbers already on the page, so it states nothing the chart is not showing.", + "Points shrunk to 3px filled dots and the line to 1.4px. Flint's 2px line with default points reads as a time series sampled twice; a slope chart has no time between its columns, so the segment is a connector, not a trend.", + "Column padding widened and the two year labels moved to the top, above the values they head — the year is a column heading here, not a measurement axis.", + "Red masthead rule, 13.5pt bold headline over a grey deck, 9.5pt labels throughout: the house identity is fixed per publication, not chosen per chart." + ], + "background": "#ffffff", + "padding": { "left": 6, "top": 4, "right": 12, "bottom": 6 }, + "title": { + "text": "Two decades of longer lives", + "subtitle": ["Life expectancy at birth, years, 2000 and 2021"] + }, + "spacing": 8, + "vconcat": [ + { + "data": { "values": [{}] }, + "mark": { "type": "rect", "fill": "#e3120b", "stroke": null }, + "width": 26, + "height": 3, + "view": { "stroke": null } + }, + { + "width": 150, + "height": 250, + "data": { + "values": [ + { "Country": "China", "Year": "2000", "Life expectancy": 72 }, + { "Country": "China", "Year": "2021", "Life expectancy": 78.2 }, + { "Country": "India", "Year": "2000", "Life expectancy": 62.5 }, + { "Country": "India", "Year": "2021", "Life expectancy": 67.2 }, + { "Country": "United States", "Year": "2000", "Life expectancy": 76.6 }, + { "Country": "United States", "Year": "2021", "Life expectancy": 76.3 }, + { "Country": "Nigeria", "Year": "2000", "Life expectancy": 46.6 }, + { "Country": "Nigeria", "Year": "2021", "Life expectancy": 52.7 }, + { "Country": "Japan", "Year": "2000", "Life expectancy": 81.1 }, + { "Country": "Japan", "Year": "2021", "Life expectancy": 84.5 }, + { "Country": "Russia", "Year": "2000", "Life expectancy": 65.5 }, + { "Country": "Russia", "Year": "2021", "Life expectancy": 70.1 }, + { "Country": "Brazil", "Year": "2000", "Life expectancy": 70.1 }, + { "Country": "Brazil", "Year": "2021", "Life expectancy": 72.8 } + ] + }, + "transform": [ + { + "window": [{ "op": "last_value", "field": "Life expectancy", "as": "__end" }], + "frame": [null, null], + "sort": [{ "field": "Year", "order": "ascending" }], + "groupby": ["Country"] + }, + { + "window": [{ "op": "first_value", "field": "Life expectancy", "as": "__start" }], + "frame": [null, null], + "sort": [{ "field": "Year", "order": "ascending" }], + "groupby": ["Country"] + }, + { "calculate": "datum.__end >= datum.__start ? 'up' : 'down'", "as": "__direction" }, + { "calculate": "datum.Country + ' ' + format(datum['Life expectancy'], '.1f')", "as": "__label" } + ], + "encoding": { + "x": { + "field": "Year", + "type": "ordinal", + "sort": ["2000", "2021"], + "title": null, + "scale": { "padding": 0.5 }, + "axis": { + "orient": "top", + "labelAngle": 0, + "labelFontSize": 10, + "labelFontWeight": 700, + "labelColor": "#121317", + "labelPadding": 8, + "domain": false, + "ticks": false, + "grid": false + } + }, + "y": { + "field": "Life expectancy", + "type": "quantitative", + "title": null, + "scale": { "zero": false, "nice": false, "domain": [44, 87] }, + "axis": null + }, + "color": { + "field": "__direction", + "type": "nominal", + "legend": null, + "scale": { "domain": ["up", "down"], "range": ["#006ba2", "#e3120b"] } + }, + "detail": { "field": "Country", "type": "nominal" } + }, + "layer": [ + { "mark": { "type": "line", "strokeWidth": 1.4 } }, + { "mark": { "type": "point", "filled": true, "size": 22 } }, + { + "transform": [{ "filter": "datum.Year === '2000'" }], + "mark": { "type": "text", "align": "right", "baseline": "middle", "dx": -7, "fontSize": 9 }, + "encoding": { "text": { "field": "__label", "type": "nominal" } } + }, + { + "transform": [{ "filter": "datum.Year === '2021'" }], + "mark": { "type": "text", "align": "left", "baseline": "middle", "dx": 7, "fontSize": 9 }, + "encoding": { "text": { "field": "__label", "type": "nominal" } } + } + ] + } + ], + "config": { + "background": "#ffffff", + "font": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "title": { + "font": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "fontSize": 13.5, + "fontWeight": 700, + "color": "#121317", + "anchor": "start", + "offset": 10, + "subtitleFont": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "subtitleFontSize": 10.5, + "subtitleColor": "#54585a", + "subtitlePadding": 6 + }, + "view": { "stroke": null }, + "axis": { + "labelFont": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "labelFontSize": 9.5, + "labelColor": "#54585a", + "grid": false, + "domain": false, + "ticks": false + } + } +} diff --git a/site/src/playground/theme-lab-assets/life-expectancy.flint.json b/site/src/playground/theme-lab-assets/life-expectancy.flint.json new file mode 100644 index 00000000..304bc533 --- /dev/null +++ b/site/src/playground/theme-lab-assets/life-expectancy.flint.json @@ -0,0 +1,149 @@ +{ + "mark": { + "type": "line", + "point": true, + "interpolate": "linear", + "strokeWidth": 2 + }, + "encoding": { + "x": { + "field": "Year", + "type": "ordinal", + "sort": [ + "2000", + "2021" + ], + "scale": { + "padding": 0.4 + } + }, + "y": { + "field": "Life expectancy", + "type": "quantitative", + "scale": { + "zero": false, + "nice": true, + "padding": 12 + }, + "axis": { + "format": ",.12~g" + } + }, + "color": { + "field": "Country", + "type": "nominal", + "sort": null, + "scale": { + "scheme": "set2" + } + } + }, + "config": { + "view": { + "continuousWidth": 340, + "continuousHeight": 230 + }, + "axisX": { + "labelLimit": 100, + "labelFontSize": 10, + "titleFontSize": 11, + "labelAngle": 0, + "labelAlign": "center", + "labelBaseline": "top" + }, + "axisY": { + "labelFontSize": 10, + "titleFontSize": 11 + }, + "legend": { + "labelFontSize": 10, + "titleFontSize": 11 + }, + "facet": { + "spacing": 23 + } + }, + "width": { + "step": 136 + }, + "data": { + "values": [ + { + "Country": "China", + "Year": "2000", + "Life expectancy": 72 + }, + { + "Country": "China", + "Year": "2021", + "Life expectancy": 78.2 + }, + { + "Country": "India", + "Year": "2000", + "Life expectancy": 62.5 + }, + { + "Country": "India", + "Year": "2021", + "Life expectancy": 67.2 + }, + { + "Country": "United States", + "Year": "2000", + "Life expectancy": 76.6 + }, + { + "Country": "United States", + "Year": "2021", + "Life expectancy": 76.3 + }, + { + "Country": "Nigeria", + "Year": "2000", + "Life expectancy": 46.6 + }, + { + "Country": "Nigeria", + "Year": "2021", + "Life expectancy": 52.7 + }, + { + "Country": "Japan", + "Year": "2000", + "Life expectancy": 81.1 + }, + { + "Country": "Japan", + "Year": "2021", + "Life expectancy": 84.5 + }, + { + "Country": "Russia", + "Year": "2000", + "Life expectancy": 65.5 + }, + { + "Country": "Russia", + "Year": "2021", + "Life expectancy": 70.1 + }, + { + "Country": "Brazil", + "Year": "2000", + "Life expectancy": 70.1 + }, + { + "Country": "Brazil", + "Year": "2021", + "Life expectancy": 72.8 + } + ] + }, + "title": { + "text": "Two decades of longer lives", + "subtitle": [ + "Life expectancy at birth, years, 2000 and 2021" + ] + } +} diff --git a/site/src/playground/theme-lab-assets/lifeexp-dumbbell.flint.json b/site/src/playground/theme-lab-assets/lifeexp-dumbbell.flint.json new file mode 100644 index 00000000..15463ea0 --- /dev/null +++ b/site/src/playground/theme-lab-assets/lifeexp-dumbbell.flint.json @@ -0,0 +1,142 @@ +{ + "encoding": { + "x": { + "field": "Life expectancy", + "type": "quantitative", + "scale": { + "zero": false + }, + "axis": { + "format": ",.12~g" + } + }, + "y": { + "field": "Country", + "type": "nominal", + "sort": null + } + }, + "layer": [ + { + "mark": "line", + "encoding": { + "detail": { + "field": "Country", + "type": "nominal", + "sort": null + } + } + }, + { + "mark": { + "type": "point", + "filled": true + }, + "encoding": { + "color": { + "field": "Sex", + "type": "nominal", + "sort": null, + "scale": { + "scheme": "tableau10" + } + } + } + } + ], + "config": { + "view": { + "continuousWidth": 280, + "continuousHeight": 230 + }, + "axisX": { + "labelLimit": 100, + "labelFontSize": 10, + "titleFontSize": 11 + }, + "axisY": { + "labelFontSize": 10, + "titleFontSize": 11 + }, + "legend": { + "labelFontSize": 10, + "titleFontSize": 11 + }, + "facet": { + "spacing": 23 + } + }, + "height": { + "step": 23 + }, + "data": { + "values": [ + { + "Country": "Japan", + "Sex": "Male", + "Life expectancy": 81.5 + }, + { + "Country": "Japan", + "Sex": "Female", + "Life expectancy": 87.6 + }, + { + "Country": "United States", + "Sex": "Male", + "Life expectancy": 73.5 + }, + { + "Country": "United States", + "Sex": "Female", + "Life expectancy": 79.3 + }, + { + "Country": "India", + "Sex": "Male", + "Life expectancy": 66 + }, + { + "Country": "India", + "Sex": "Female", + "Life expectancy": 69 + }, + { + "Country": "Brazil", + "Sex": "Male", + "Life expectancy": 69 + }, + { + "Country": "Brazil", + "Sex": "Female", + "Life expectancy": 76 + }, + { + "Country": "Nigeria", + "Sex": "Male", + "Life expectancy": 51 + }, + { + "Country": "Nigeria", + "Sex": "Female", + "Life expectancy": 54 + }, + { + "Country": "Germany", + "Sex": "Male", + "Life expectancy": 78.5 + }, + { + "Country": "Germany", + "Sex": "Female", + "Life expectancy": 83.4 + } + ] + }, + "title": { + "text": "Women outlive men everywhere, but not by the same margin", + "subtitle": [ + "Life expectancy at birth by sex, 2021, years" + ] + } +} diff --git a/site/src/playground/theme-lab-assets/lifeexp-dumbbell.mckinsey.json b/site/src/playground/theme-lab-assets/lifeexp-dumbbell.mckinsey.json new file mode 100644 index 00000000..19e2ccc5 --- /dev/null +++ b/site/src/playground/theme-lab-assets/lifeexp-dumbbell.mckinsey.json @@ -0,0 +1,157 @@ +{ + "$schema": "https://vega-lite.github.io/schema/vega-lite/v5.json", + "__theme__": "mckinsey", + "__design__": [ + "Row order left exactly as the baseline has it — Japan, United States, India, Brazil, Nigeria, Germany. Sorting by the gap would make the chart answer its own headline, which is exactly why a theme may not do it: which country follows which is a statement about the data, not a house style.", + "The gap is stated as a number at the end of each row. It is the subject of the chart and Flint asks the reader to measure it off a shared axis by eye.", + "tableau10 replaced by two colours from the house palette: deep navy #051C2C for men, brand blue #2251FF for women. Neither is greyed out — the comparison is symmetric and this is one of the rare cases where the 'one series in blue, the rest in grey' rule would assert something the data does not.", + "Legend deleted and each dot named inline, in the colour it names, on the first row only. This is a label attached to a mark, not a legend moved to a new place: it sits at its own dot's position and the reader picks the name up while reading the row. Two dots do not justify a lookup table. The exact offset above the dot is implementation — the decision is inline-at-the-mark.", + "The connecting line demoted to a 3px #D3DCE1 bar behind the dots. Flint draws it as a default-blue line at the same visual weight as the markers, so the connector competes with the endpoints it exists to join.", + "The value axis kept, against house rule, and reduced to five unlabelled-title ticks. A dumbbell means nothing without absolute position — 51 to 54 and 81.5 to 87.6 are different facts. This is the one chart in the McKinsey set where deleting the axis would destroy the reading, and the numbers on the marks carry the gap instead.", + "Flint's ',.12~g' axis format dropped for plain integers." + ], + "background": "#ffffff", + "padding": { "left": 8, "top": 6, "right": 20, "bottom": 8 }, + "title": { + "text": "Women outlive men everywhere, but not by the same margin", + "subtitle": ["Life expectancy at birth by sex, 2021, years"] + }, + "width": 250, + "height": 178, + "data": { + "values": [ + { "Country": "Japan", "Sex": "Male", "Life expectancy": 81.5 }, + { "Country": "Japan", "Sex": "Female", "Life expectancy": 87.6 }, + { "Country": "United States", "Sex": "Male", "Life expectancy": 73.5 }, + { "Country": "United States", "Sex": "Female", "Life expectancy": 79.3 }, + { "Country": "India", "Sex": "Male", "Life expectancy": 66 }, + { "Country": "India", "Sex": "Female", "Life expectancy": 69 }, + { "Country": "Brazil", "Sex": "Male", "Life expectancy": 69 }, + { "Country": "Brazil", "Sex": "Female", "Life expectancy": 76 }, + { "Country": "Nigeria", "Sex": "Male", "Life expectancy": 51 }, + { "Country": "Nigeria", "Sex": "Female", "Life expectancy": 54 }, + { "Country": "Germany", "Sex": "Male", "Life expectancy": 78.5 }, + { "Country": "Germany", "Sex": "Female", "Life expectancy": 83.4 } + ] + }, + "transform": [ + { + "joinaggregate": [ + { "op": "min", "field": "Life expectancy", "as": "__lo" }, + { "op": "max", "field": "Life expectancy", "as": "__hi" } + ], + "groupby": ["Country"] + }, + { "calculate": "datum.__hi - datum.__lo", "as": "__gap" }, + { "calculate": "'+' + format(datum.__gap, '.1f')", "as": "__gapLabel" } + ], + "encoding": { + "y": { + "field": "Country", + "type": "nominal", + "title": null, + "sort": null, + "scale": { "paddingInner": 0.6, "paddingOuter": 0.55 }, + "axis": { "domain": false, "ticks": false, "grid": false, "labelPadding": 6 } + }, + "x": { + "field": "Life expectancy", + "type": "quantitative", + "title": null, + "scale": { "zero": false, "nice": false, "domain": [46, 94] }, + "axis": { + "values": [50, 60, 70, 80, 90], + "format": "d", + "grid": false, + "domain": false, + "ticks": false, + "labelColor": "#8fa0ab", + "labelPadding": 6 + } + } + }, + "layer": [ + { + "mark": { "type": "bar", "height": 3, "color": "#d3dce1" }, + "encoding": { + "x": { "field": "__lo", "type": "quantitative" }, + "x2": { "field": "__hi" } + } + }, + { + "mark": { "type": "point", "filled": true, "size": 90, "stroke": "#ffffff", "strokeWidth": 1.2 }, + "encoding": { + "color": { + "field": "Sex", + "type": "nominal", + "scale": { "domain": ["Male", "Female"], "range": ["#051c2c", "#2251ff"] }, + "legend": null + } + } + }, + { + "mark": { + "type": "text", + "align": "left", + "baseline": "middle", + "dx": 9, + "font": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "fontSize": 10, + "fontWeight": 600, + "color": "#051c2c" + }, + "encoding": { + "x": { "field": "__hi", "type": "quantitative" }, + "text": { "field": "__gapLabel", "type": "nominal" } + } + }, + { + "transform": [{ "filter": "datum.Country === 'Japan'" }], + "mark": { + "type": "text", + "baseline": "bottom", + "dy": -12, + "font": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "fontSize": 10, + "fontWeight": 600 + }, + "encoding": { + "text": { "field": "Sex", "type": "nominal" }, + "color": { + "field": "Sex", + "type": "nominal", + "scale": { "domain": ["Male", "Female"], "range": ["#051c2c", "#2251ff"] }, + "legend": null + } + } + } + ], + "config": { + "background": "#ffffff", + "font": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "title": { + "font": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "fontSize": 14, + "fontWeight": 700, + "color": "#051c2c", + "anchor": "start", + "offset": 12, + "subtitleFont": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "subtitleFontSize": 10.5, + "subtitleColor": "#5c6b75", + "subtitlePadding": 6 + }, + "view": { "stroke": null }, + "axis": { + "labelFont": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "labelFontSize": 10, + "labelColor": "#051c2c", + "titleFont": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "titleFontSize": 9, + "titleColor": "#8fa0ab", + "grid": false, + "domain": false, + "ticks": false + } + } +} diff --git a/site/src/playground/theme-lab-assets/marathon-wr.flint.json b/site/src/playground/theme-lab-assets/marathon-wr.flint.json new file mode 100644 index 00000000..72d0f83b --- /dev/null +++ b/site/src/playground/theme-lab-assets/marathon-wr.flint.json @@ -0,0 +1,112 @@ +{ + "mark": "line", + "encoding": { + "x": { + "field": "Year", + "type": "temporal" + }, + "y": { + "field": "Record (min)", + "type": "quantitative", + "scale": { + "zero": true + }, + "axis": { + "format": ",.12~g" + } + } + }, + "config": { + "view": { + "continuousWidth": 308, + "continuousHeight": 254 + }, + "axisX": { + "labelLimit": 100, + "labelFontSize": 10, + "titleFontSize": 11 + }, + "axisY": { + "labelFontSize": 10, + "titleFontSize": 11 + }, + "legend": { + "labelFontSize": 10, + "titleFontSize": 11 + }, + "facet": { + "spacing": 25 + } + }, + "data": { + "values": [ + { + "Year": "1908", + "Record (min)": 175 + }, + { + "Year": "1925", + "Record (min)": 173 + }, + { + "Year": "1935", + "Record (min)": 166 + }, + { + "Year": "1947", + "Record (min)": 165 + }, + { + "Year": "1958", + "Record (min)": 155 + }, + { + "Year": "1967", + "Record (min)": 150 + }, + { + "Year": "1969", + "Record (min)": 148 + }, + { + "Year": "1988", + "Record (min)": 127 + }, + { + "Year": "1998", + "Record (min)": 126 + }, + { + "Year": "2003", + "Record (min)": 125 + }, + { + "Year": "2008", + "Record (min)": 124 + }, + { + "Year": "2011", + "Record (min)": 123.6 + }, + { + "Year": "2014", + "Record (min)": 122.9 + }, + { + "Year": "2018", + "Record (min)": 121.6 + }, + { + "Year": "2022", + "Record (min)": 121.1 + }, + { + "Year": "2023", + "Record (min)": 120.6 + } + ] + }, + "title": { + "text": "Men's marathon world record, 1908–2023 (minutes)" + } +} diff --git a/site/src/playground/theme-lab-assets/medals-grouped.flint.json b/site/src/playground/theme-lab-assets/medals-grouped.flint.json new file mode 100644 index 00000000..50b596e7 --- /dev/null +++ b/site/src/playground/theme-lab-assets/medals-grouped.flint.json @@ -0,0 +1,141 @@ +{ + "mark": "bar", + "encoding": { + "x": { + "field": "Country", + "type": "nominal", + "sort": null + }, + "y": { + "field": "Count", + "type": "quantitative", + "scale": { + "zero": true + }, + "axis": { + "format": ",.12~g" + } + }, + "color": { + "field": "Medal", + "type": "nominal", + "sort": null, + "scale": { + "scheme": "tableau10" + } + }, + "xOffset": { + "field": "Medal", + "type": "nominal", + "sort": null + } + }, + "config": { + "view": { + "continuousWidth": 340, + "continuousHeight": 230 + }, + "axisX": { + "labelLimit": 100, + "labelFontSize": 10, + "titleFontSize": 11 + }, + "axisY": { + "labelFontSize": 10, + "titleFontSize": 11 + }, + "legend": { + "labelFontSize": 10, + "titleFontSize": 11 + }, + "facet": { + "spacing": 23 + } + }, + "width": { + "step": 68, + "for": "position" + }, + "data": { + "values": [ + { + "Country": "United States", + "Medal": "Gold", + "Count": 40 + }, + { + "Country": "United States", + "Medal": "Silver", + "Count": 44 + }, + { + "Country": "United States", + "Medal": "Bronze", + "Count": 42 + }, + { + "Country": "China", + "Medal": "Gold", + "Count": 40 + }, + { + "Country": "China", + "Medal": "Silver", + "Count": 27 + }, + { + "Country": "China", + "Medal": "Bronze", + "Count": 24 + }, + { + "Country": "Japan", + "Medal": "Gold", + "Count": 20 + }, + { + "Country": "Japan", + "Medal": "Silver", + "Count": 12 + }, + { + "Country": "Japan", + "Medal": "Bronze", + "Count": 13 + }, + { + "Country": "Australia", + "Medal": "Gold", + "Count": 18 + }, + { + "Country": "Australia", + "Medal": "Silver", + "Count": 19 + }, + { + "Country": "Australia", + "Medal": "Bronze", + "Count": 16 + }, + { + "Country": "France", + "Medal": "Gold", + "Count": 16 + }, + { + "Country": "France", + "Medal": "Silver", + "Count": 26 + }, + { + "Country": "France", + "Medal": "Bronze", + "Count": 22 + } + ] + }, + "title": { + "text": "Paris 2024 Olympic medals — top nations" + } +} diff --git a/site/src/playground/theme-lab-assets/mobile-donut.flint.json b/site/src/playground/theme-lab-assets/mobile-donut.flint.json new file mode 100644 index 00000000..db2eae3b --- /dev/null +++ b/site/src/playground/theme-lab-assets/mobile-donut.flint.json @@ -0,0 +1,60 @@ +{ + "mark": "arc", + "encoding": { + "theta": { + "field": "Share", + "type": "quantitative" + }, + "color": { + "field": "OS", + "type": "nominal", + "sort": null, + "scale": { + "scheme": "tableau10" + } + } + }, + "width": 346, + "height": 346, + "config": { + "view": { + "continuousWidth": 346, + "continuousHeight": 346 + }, + "axisX": { + "labelLimit": 100, + "labelFontSize": 10, + "titleFontSize": 11 + }, + "axisY": { + "labelFontSize": 10, + "titleFontSize": 11 + }, + "legend": { + "labelFontSize": 10, + "titleFontSize": 11 + }, + "facet": { + "spacing": 23 + } + }, + "data": { + "values": [ + { + "OS": "Android", + "Share": 71 + }, + { + "OS": "iOS", + "Share": 28 + }, + { + "OS": "Other", + "Share": 1 + } + ] + }, + "title": { + "text": "Mobile OS market share, 2024" + } +} diff --git a/site/src/playground/theme-lab-assets/nutrition-radar.flint.json b/site/src/playground/theme-lab-assets/nutrition-radar.flint.json new file mode 100644 index 00000000..bf041d15 --- /dev/null +++ b/site/src/playground/theme-lab-assets/nutrition-radar.flint.json @@ -0,0 +1,979 @@ +{ + "width": 230, + "height": 230, + "layer": [ + { + "data": { + "values": [ + { + "__type": "spoke", + "__x": 0, + "__y": 0, + "__x2": 0, + "__y2": -1 + }, + { + "__type": "spoke", + "__x": 0, + "__y": 0, + "__x2": 0.9510565162951535, + "__y2": -0.30901699437494745 + }, + { + "__type": "spoke", + "__x": 0, + "__y": 0, + "__x2": 0.5877852522924732, + "__y2": 0.8090169943749473 + }, + { + "__type": "spoke", + "__x": 0, + "__y": 0, + "__x2": -0.587785252292473, + "__y2": 0.8090169943749475 + }, + { + "__type": "spoke", + "__x": 0, + "__y": 0, + "__x2": -0.9510565162951536, + "__y2": -0.30901699437494723 + } + ] + }, + "mark": { + "type": "rule", + "stroke": "#ddd", + "strokeWidth": 0.8 + }, + "encoding": { + "x": { + "field": "__x", + "type": "quantitative", + "scale": { + "domain": [ + -1.18, + 1.18 + ] + }, + "axis": null + }, + "y": { + "field": "__y", + "type": "quantitative", + "scale": { + "domain": [ + -1.18, + 1.18 + ] + }, + "axis": null + }, + "x2": { + "field": "__x2" + }, + "y2": { + "field": "__y2" + } + } + }, + { + "data": { + "values": [ + { + "__type": "ring", + "__level": 0.25, + "__x": 0, + "__y": -0.25, + "__x2": 0.23776412907378838, + "__y2": -0.07725424859373686 + }, + { + "__type": "ring", + "__level": 0.25, + "__x": 0.23776412907378838, + "__y": -0.07725424859373686, + "__x2": 0.1469463130731183, + "__y2": 0.20225424859373684 + }, + { + "__type": "ring", + "__level": 0.25, + "__x": 0.1469463130731183, + "__y": 0.20225424859373684, + "__x2": -0.14694631307311826, + "__y2": 0.20225424859373686 + }, + { + "__type": "ring", + "__level": 0.25, + "__x": -0.14694631307311826, + "__y": 0.20225424859373686, + "__x2": -0.2377641290737884, + "__y2": -0.07725424859373681 + }, + { + "__type": "ring", + "__level": 0.25, + "__x": -0.2377641290737884, + "__y": -0.07725424859373681, + "__x2": 0, + "__y2": -0.25 + }, + { + "__type": "ring", + "__level": 0.5, + "__x": 0, + "__y": -0.5, + "__x2": 0.47552825814757677, + "__y2": -0.15450849718747373 + }, + { + "__type": "ring", + "__level": 0.5, + "__x": 0.47552825814757677, + "__y": -0.15450849718747373, + "__x2": 0.2938926261462366, + "__y2": 0.40450849718747367 + }, + { + "__type": "ring", + "__level": 0.5, + "__x": 0.2938926261462366, + "__y": 0.40450849718747367, + "__x2": -0.2938926261462365, + "__y2": 0.4045084971874737 + }, + { + "__type": "ring", + "__level": 0.5, + "__x": -0.2938926261462365, + "__y": 0.4045084971874737, + "__x2": -0.4755282581475768, + "__y2": -0.15450849718747361 + }, + { + "__type": "ring", + "__level": 0.5, + "__x": -0.4755282581475768, + "__y": -0.15450849718747361, + "__x2": 0, + "__y2": -0.5 + }, + { + "__type": "ring", + "__level": 0.75, + "__x": 0, + "__y": -0.75, + "__x2": 0.7132923872213651, + "__y2": -0.2317627457812106 + }, + { + "__type": "ring", + "__level": 0.75, + "__x": 0.7132923872213651, + "__y": -0.2317627457812106, + "__x2": 0.4408389392193549, + "__y2": 0.6067627457812105 + }, + { + "__type": "ring", + "__level": 0.75, + "__x": 0.4408389392193549, + "__y": 0.6067627457812105, + "__x2": -0.4408389392193548, + "__y2": 0.6067627457812106 + }, + { + "__type": "ring", + "__level": 0.75, + "__x": -0.4408389392193548, + "__y": 0.6067627457812106, + "__x2": -0.7132923872213652, + "__y2": -0.23176274578121042 + }, + { + "__type": "ring", + "__level": 0.75, + "__x": -0.7132923872213652, + "__y": -0.23176274578121042, + "__x2": 0, + "__y2": -0.75 + }, + { + "__type": "ring", + "__level": 1, + "__x": 0, + "__y": -1, + "__x2": 0.9510565162951535, + "__y2": -0.30901699437494745 + }, + { + "__type": "ring", + "__level": 1, + "__x": 0.9510565162951535, + "__y": -0.30901699437494745, + "__x2": 0.5877852522924732, + "__y2": 0.8090169943749473 + }, + { + "__type": "ring", + "__level": 1, + "__x": 0.5877852522924732, + "__y": 0.8090169943749473, + "__x2": -0.587785252292473, + "__y2": 0.8090169943749475 + }, + { + "__type": "ring", + "__level": 1, + "__x": -0.587785252292473, + "__y": 0.8090169943749475, + "__x2": -0.9510565162951536, + "__y2": -0.30901699437494723 + }, + { + "__type": "ring", + "__level": 1, + "__x": -0.9510565162951536, + "__y": -0.30901699437494723, + "__x2": 0, + "__y2": -1 + } + ] + }, + "mark": { + "type": "rule", + "stroke": "#e0e0e0", + "strokeWidth": 0.6 + }, + "encoding": { + "x": { + "field": "__x", + "type": "quantitative", + "axis": null + }, + "y": { + "field": "__y", + "type": "quantitative", + "axis": null + }, + "x2": { + "field": "__x2" + }, + "y2": { + "field": "__y2" + } + } + }, + { + "data": { + "values": [ + { + "__label": [ + "Protein", + "(25)" + ], + "__x": 0, + "__y": -1.15, + "__align": "center", + "__baseline": "bottom", + "__dx": 0, + "__dy": -4 + } + ] + }, + "mark": { + "type": "text", + "fontSize": 10, + "fill": "#555", + "align": "center", + "baseline": "bottom", + "dx": 0, + "dy": -4, + "limit": 120, + "lineHeight": 13 + }, + "encoding": { + "x": { + "field": "__x", + "type": "quantitative", + "axis": null + }, + "y": { + "field": "__y", + "type": "quantitative", + "axis": null + }, + "text": { + "value": [ + "Protein", + "(25)" + ] + } + } + }, + { + "data": { + "values": [ + { + "__label": [ + "Fat", + "(50)" + ], + "__x": 1.0937149937394264, + "__y": -0.35536954353118955, + "__align": "left", + "__baseline": "bottom", + "__dx": 4, + "__dy": 0 + } + ] + }, + "mark": { + "type": "text", + "fontSize": 10, + "fill": "#555", + "align": "left", + "baseline": "bottom", + "dx": 4, + "dy": 0, + "limit": 120, + "lineHeight": 13 + }, + "encoding": { + "x": { + "field": "__x", + "type": "quantitative", + "axis": null + }, + "y": { + "field": "__y", + "type": "quantitative", + "axis": null + }, + "text": { + "value": [ + "Fat", + "(50)" + ] + } + } + }, + { + "data": { + "values": [ + { + "__label": [ + "Carbs", + "(100)" + ], + "__x": 0.6759530401363442, + "__y": 0.9303695435311894, + "__align": "left", + "__baseline": "top", + "__dx": 4, + "__dy": 0 + } + ] + }, + "mark": { + "type": "text", + "fontSize": 10, + "fill": "#555", + "align": "left", + "baseline": "top", + "dx": 4, + "dy": 0, + "limit": 120, + "lineHeight": 13 + }, + "encoding": { + "x": { + "field": "__x", + "type": "quantitative", + "axis": null + }, + "y": { + "field": "__y", + "type": "quantitative", + "axis": null + }, + "text": { + "value": [ + "Carbs", + "(100)" + ] + } + } + }, + { + "data": { + "values": [ + { + "__label": [ + "Fiber", + "(20)" + ], + "__x": -0.6759530401363439, + "__y": 0.9303695435311895, + "__align": "right", + "__baseline": "top", + "__dx": -4, + "__dy": 0 + } + ] + }, + "mark": { + "type": "text", + "fontSize": 10, + "fill": "#555", + "align": "right", + "baseline": "top", + "dx": -4, + "dy": 0, + "limit": 120, + "lineHeight": 13 + }, + "encoding": { + "x": { + "field": "__x", + "type": "quantitative", + "axis": null + }, + "y": { + "field": "__y", + "type": "quantitative", + "axis": null + }, + "text": { + "value": [ + "Fiber", + "(20)" + ] + } + } + }, + { + "data": { + "values": [ + { + "__label": [ + "Sugar", + "(5)" + ], + "__x": -1.0937149937394266, + "__y": -0.35536954353118927, + "__align": "right", + "__baseline": "bottom", + "__dx": -4, + "__dy": 0 + } + ] + }, + "mark": { + "type": "text", + "fontSize": 10, + "fill": "#555", + "align": "right", + "baseline": "bottom", + "dx": -4, + "dy": 0, + "limit": 120, + "lineHeight": 13 + }, + "encoding": { + "x": { + "field": "__x", + "type": "quantitative", + "axis": null + }, + "y": { + "field": "__y", + "type": "quantitative", + "axis": null + }, + "text": { + "value": [ + "Sugar", + "(5)" + ] + } + } + }, + { + "data": { + "values": [ + { + "__group": "Almonds", + "__axis": "Protein", + "__value": 0.84, + "__raw": 21, + "__angle": 0, + "__x": 0, + "__y": -0.84 + }, + { + "__group": "Almonds", + "__axis": "Fat", + "__value": 1, + "__raw": 50, + "__angle": 72, + "__x": 0.9510565162951535, + "__y": -0.30901699437494745 + }, + { + "__group": "Almonds", + "__axis": "Carbs", + "__value": 0.22, + "__raw": 22, + "__angle": 144, + "__x": 0.12931275550434412, + "__y": 0.1779837387624884 + }, + { + "__group": "Almonds", + "__axis": "Fiber", + "__value": 0.6, + "__raw": 12, + "__angle": 216, + "__x": -0.3526711513754838, + "__y": 0.4854101966249684 + }, + { + "__group": "Almonds", + "__axis": "Sugar", + "__value": 0.8, + "__raw": 4, + "__angle": 288, + "__x": -0.760845213036123, + "__y": -0.2472135954999578 + }, + { + "__group": "Oats", + "__axis": "Protein", + "__value": 0.68, + "__raw": 17, + "__angle": 0, + "__x": 0, + "__y": -0.68 + }, + { + "__group": "Oats", + "__axis": "Fat", + "__value": 0.14, + "__raw": 7, + "__angle": 72, + "__x": 0.1331479122813215, + "__y": -0.043262379212492645 + }, + { + "__group": "Oats", + "__axis": "Carbs", + "__value": 0.66, + "__raw": 66, + "__angle": 144, + "__x": 0.38793826651303237, + "__y": 0.5339512162874652 + }, + { + "__group": "Oats", + "__axis": "Fiber", + "__value": 0.55, + "__raw": 11, + "__angle": 216, + "__x": -0.32328188876086017, + "__y": 0.44495934690622113 + }, + { + "__group": "Oats", + "__axis": "Sugar", + "__value": 0.2, + "__raw": 1, + "__angle": 288, + "__x": -0.19021130325903074, + "__y": -0.06180339887498945 + }, + { + "__group": "Greek yogurt", + "__axis": "Protein", + "__value": 0.4, + "__raw": 10, + "__angle": 0, + "__x": 0, + "__y": -0.4 + }, + { + "__group": "Greek yogurt", + "__axis": "Fat", + "__value": 0.1, + "__raw": 5, + "__angle": 72, + "__x": 0.09510565162951536, + "__y": -0.030901699437494747 + }, + { + "__group": "Greek yogurt", + "__axis": "Carbs", + "__value": 0.04, + "__raw": 4, + "__angle": 144, + "__x": 0.02351141009169893, + "__y": 0.032360679774997896 + }, + { + "__group": "Greek yogurt", + "__axis": "Fiber", + "__value": 0, + "__raw": 0, + "__angle": 216, + "__x": 0, + "__y": 0 + }, + { + "__group": "Greek yogurt", + "__axis": "Sugar", + "__value": 0.8, + "__raw": 4, + "__angle": 288, + "__x": -0.760845213036123, + "__y": -0.2472135954999578 + } + ] + }, + "mark": { + "type": "line", + "interpolate": "linear-closed", + "strokeWidth": 1.5, + "point": false, + "fillOpacity": 0.15 + }, + "encoding": { + "x": { + "field": "__x", + "type": "quantitative", + "axis": null + }, + "y": { + "field": "__y", + "type": "quantitative", + "axis": null + }, + "order": { + "field": "__angle", + "type": "quantitative" + }, + "tooltip": [ + { + "field": "__axis", + "type": "nominal", + "title": "Nutrient" + }, + { + "field": "__raw", + "type": "quantitative", + "title": "Grams/100g" + } + ], + "stroke": { + "field": "__group", + "type": "nominal", + "title": "Food" + }, + "fill": { + "field": "__group", + "type": "nominal", + "title": "Food", + "legend": null + } + } + }, + { + "data": { + "values": [ + { + "__group": "Almonds", + "__axis": "Protein", + "__value": 0.84, + "__raw": 21, + "__angle": 0, + "__x": 0, + "__y": -0.84 + }, + { + "__group": "Almonds", + "__axis": "Fat", + "__value": 1, + "__raw": 50, + "__angle": 72, + "__x": 0.9510565162951535, + "__y": -0.30901699437494745 + }, + { + "__group": "Almonds", + "__axis": "Carbs", + "__value": 0.22, + "__raw": 22, + "__angle": 144, + "__x": 0.12931275550434412, + "__y": 0.1779837387624884 + }, + { + "__group": "Almonds", + "__axis": "Fiber", + "__value": 0.6, + "__raw": 12, + "__angle": 216, + "__x": -0.3526711513754838, + "__y": 0.4854101966249684 + }, + { + "__group": "Almonds", + "__axis": "Sugar", + "__value": 0.8, + "__raw": 4, + "__angle": 288, + "__x": -0.760845213036123, + "__y": -0.2472135954999578 + }, + { + "__group": "Oats", + "__axis": "Protein", + "__value": 0.68, + "__raw": 17, + "__angle": 0, + "__x": 0, + "__y": -0.68 + }, + { + "__group": "Oats", + "__axis": "Fat", + "__value": 0.14, + "__raw": 7, + "__angle": 72, + "__x": 0.1331479122813215, + "__y": -0.043262379212492645 + }, + { + "__group": "Oats", + "__axis": "Carbs", + "__value": 0.66, + "__raw": 66, + "__angle": 144, + "__x": 0.38793826651303237, + "__y": 0.5339512162874652 + }, + { + "__group": "Oats", + "__axis": "Fiber", + "__value": 0.55, + "__raw": 11, + "__angle": 216, + "__x": -0.32328188876086017, + "__y": 0.44495934690622113 + }, + { + "__group": "Oats", + "__axis": "Sugar", + "__value": 0.2, + "__raw": 1, + "__angle": 288, + "__x": -0.19021130325903074, + "__y": -0.06180339887498945 + }, + { + "__group": "Greek yogurt", + "__axis": "Protein", + "__value": 0.4, + "__raw": 10, + "__angle": 0, + "__x": 0, + "__y": -0.4 + }, + { + "__group": "Greek yogurt", + "__axis": "Fat", + "__value": 0.1, + "__raw": 5, + "__angle": 72, + "__x": 0.09510565162951536, + "__y": -0.030901699437494747 + }, + { + "__group": "Greek yogurt", + "__axis": "Carbs", + "__value": 0.04, + "__raw": 4, + "__angle": 144, + "__x": 0.02351141009169893, + "__y": 0.032360679774997896 + }, + { + "__group": "Greek yogurt", + "__axis": "Fiber", + "__value": 0, + "__raw": 0, + "__angle": 216, + "__x": 0, + "__y": 0 + }, + { + "__group": "Greek yogurt", + "__axis": "Sugar", + "__value": 0.8, + "__raw": 4, + "__angle": 288, + "__x": -0.760845213036123, + "__y": -0.2472135954999578 + } + ] + }, + "mark": { + "type": "point", + "filled": true, + "size": 25 + }, + "encoding": { + "x": { + "field": "__x", + "type": "quantitative", + "axis": null + }, + "y": { + "field": "__y", + "type": "quantitative", + "axis": null + }, + "tooltip": [ + { + "field": "__group", + "type": "nominal", + "title": "Food" + }, + { + "field": "__axis", + "type": "nominal", + "title": "Nutrient" + }, + { + "field": "__raw", + "type": "quantitative", + "title": "Grams/100g" + } + ], + "color": { + "field": "__group", + "type": "nominal", + "title": "Food", + "legend": null + } + } + } + ], + "config": { + "view": { + "continuousWidth": 230, + "continuousHeight": 230, + "stroke": null + }, + "axisX": { + "labelLimit": 100, + "labelFontSize": 10, + "titleFontSize": 11 + }, + "axisY": { + "labelFontSize": 10, + "titleFontSize": 11 + }, + "legend": { + "labelFontSize": 10, + "titleFontSize": 11 + }, + "facet": { + "spacing": 23 + } + }, + "data": { + "values": [ + { + "Food": "Almonds", + "Nutrient": "Protein", + "Grams/100g": 21 + }, + { + "Food": "Almonds", + "Nutrient": "Fat", + "Grams/100g": 50 + }, + { + "Food": "Almonds", + "Nutrient": "Carbs", + "Grams/100g": 22 + }, + { + "Food": "Almonds", + "Nutrient": "Fiber", + "Grams/100g": 12 + }, + { + "Food": "Almonds", + "Nutrient": "Sugar", + "Grams/100g": 4 + }, + { + "Food": "Oats", + "Nutrient": "Protein", + "Grams/100g": 17 + }, + { + "Food": "Oats", + "Nutrient": "Fat", + "Grams/100g": 7 + }, + { + "Food": "Oats", + "Nutrient": "Carbs", + "Grams/100g": 66 + }, + { + "Food": "Oats", + "Nutrient": "Fiber", + "Grams/100g": 11 + }, + { + "Food": "Oats", + "Nutrient": "Sugar", + "Grams/100g": 1 + }, + { + "Food": "Greek yogurt", + "Nutrient": "Protein", + "Grams/100g": 10 + }, + { + "Food": "Greek yogurt", + "Nutrient": "Fat", + "Grams/100g": 5 + }, + { + "Food": "Greek yogurt", + "Nutrient": "Carbs", + "Grams/100g": 4 + }, + { + "Food": "Greek yogurt", + "Nutrient": "Fiber", + "Grams/100g": 0 + }, + { + "Food": "Greek yogurt", + "Nutrient": "Sugar", + "Grams/100g": 4 + } + ] + }, + "title": { + "text": "Nutrition profile per 100 g — Almonds vs Oats vs Greek yogurt" + } +} diff --git a/site/src/playground/theme-lab-assets/oecd-facet-16.flint.json b/site/src/playground/theme-lab-assets/oecd-facet-16.flint.json new file mode 100644 index 00000000..e03eb303 --- /dev/null +++ b/site/src/playground/theme-lab-assets/oecd-facet-16.flint.json @@ -0,0 +1,465 @@ +{ + "mark": "line", + "encoding": { + "x": { + "field": "Year", + "type": "temporal" + }, + "y": { + "field": "Unemployment (%)", + "type": "quantitative", + "scale": { + "zero": true + }, + "axis": { + "format": ",.12~g" + } + }, + "facet": { + "field": "Country", + "type": "nominal", + "sort": null, + "columns": 6 + } + }, + "config": { + "view": { + "continuousWidth": 67, + "continuousHeight": 75 + }, + "axisX": { + "labelLimit": 100, + "labelFontSize": 8, + "titleFontSize": 11, + "titleFontWeight": "normal", + "titleColor": "#666" + }, + "axisY": { + "labelFontSize": 8, + "titleFontSize": 11, + "titleFontWeight": "normal", + "titleColor": "#666" + }, + "legend": { + "labelFontSize": 9, + "titleFontSize": 9 + }, + "headerFacet": { + "labelFontSize": 9, + "labelLimit": 87 + }, + "facet": { + "spacing": 7 + } + }, + "data": { + "values": [ + { + "Year": "2000", + "Country": "Australia", + "Unemployment (%)": 6.3 + }, + { + "Year": "2007", + "Country": "Australia", + "Unemployment (%)": 4.4 + }, + { + "Year": "2010", + "Country": "Australia", + "Unemployment (%)": 5.2 + }, + { + "Year": "2015", + "Country": "Australia", + "Unemployment (%)": 6.1 + }, + { + "Year": "2023", + "Country": "Australia", + "Unemployment (%)": 3.7 + }, + { + "Year": "2000", + "Country": "Austria", + "Unemployment (%)": 4.7 + }, + { + "Year": "2007", + "Country": "Austria", + "Unemployment (%)": 4.9 + }, + { + "Year": "2010", + "Country": "Austria", + "Unemployment (%)": 4.8 + }, + { + "Year": "2015", + "Country": "Austria", + "Unemployment (%)": 5.7 + }, + { + "Year": "2023", + "Country": "Austria", + "Unemployment (%)": 5.1 + }, + { + "Year": "2000", + "Country": "Belgium", + "Unemployment (%)": 6.9 + }, + { + "Year": "2007", + "Country": "Belgium", + "Unemployment (%)": 7.5 + }, + { + "Year": "2010", + "Country": "Belgium", + "Unemployment (%)": 8.3 + }, + { + "Year": "2015", + "Country": "Belgium", + "Unemployment (%)": 8.5 + }, + { + "Year": "2023", + "Country": "Belgium", + "Unemployment (%)": 5.5 + }, + { + "Year": "2000", + "Country": "Canada", + "Unemployment (%)": 6.8 + }, + { + "Year": "2007", + "Country": "Canada", + "Unemployment (%)": 6 + }, + { + "Year": "2010", + "Country": "Canada", + "Unemployment (%)": 8 + }, + { + "Year": "2015", + "Country": "Canada", + "Unemployment (%)": 6.9 + }, + { + "Year": "2023", + "Country": "Canada", + "Unemployment (%)": 5.4 + }, + { + "Year": "2000", + "Country": "Denmark", + "Unemployment (%)": 4.3 + }, + { + "Year": "2007", + "Country": "Denmark", + "Unemployment (%)": 3.8 + }, + { + "Year": "2010", + "Country": "Denmark", + "Unemployment (%)": 7.5 + }, + { + "Year": "2015", + "Country": "Denmark", + "Unemployment (%)": 6.2 + }, + { + "Year": "2023", + "Country": "Denmark", + "Unemployment (%)": 5.1 + }, + { + "Year": "2000", + "Country": "Finland", + "Unemployment (%)": 9.8 + }, + { + "Year": "2007", + "Country": "Finland", + "Unemployment (%)": 6.9 + }, + { + "Year": "2010", + "Country": "Finland", + "Unemployment (%)": 8.4 + }, + { + "Year": "2015", + "Country": "Finland", + "Unemployment (%)": 9.4 + }, + { + "Year": "2023", + "Country": "Finland", + "Unemployment (%)": 7.2 + }, + { + "Year": "2000", + "Country": "France", + "Unemployment (%)": 9 + }, + { + "Year": "2007", + "Country": "France", + "Unemployment (%)": 8 + }, + { + "Year": "2010", + "Country": "France", + "Unemployment (%)": 9.3 + }, + { + "Year": "2015", + "Country": "France", + "Unemployment (%)": 10.4 + }, + { + "Year": "2023", + "Country": "France", + "Unemployment (%)": 7.3 + }, + { + "Year": "2000", + "Country": "Germany", + "Unemployment (%)": 7.9 + }, + { + "Year": "2007", + "Country": "Germany", + "Unemployment (%)": 8.7 + }, + { + "Year": "2010", + "Country": "Germany", + "Unemployment (%)": 7 + }, + { + "Year": "2015", + "Country": "Germany", + "Unemployment (%)": 4.6 + }, + { + "Year": "2023", + "Country": "Germany", + "Unemployment (%)": 3.1 + }, + { + "Year": "2000", + "Country": "Ireland", + "Unemployment (%)": 4.2 + }, + { + "Year": "2007", + "Country": "Ireland", + "Unemployment (%)": 4.7 + }, + { + "Year": "2010", + "Country": "Ireland", + "Unemployment (%)": 13.9 + }, + { + "Year": "2015", + "Country": "Ireland", + "Unemployment (%)": 9.9 + }, + { + "Year": "2023", + "Country": "Ireland", + "Unemployment (%)": 4.3 + }, + { + "Year": "2000", + "Country": "Italy", + "Unemployment (%)": 10.1 + }, + { + "Year": "2007", + "Country": "Italy", + "Unemployment (%)": 6.1 + }, + { + "Year": "2010", + "Country": "Italy", + "Unemployment (%)": 8.4 + }, + { + "Year": "2015", + "Country": "Italy", + "Unemployment (%)": 11.9 + }, + { + "Year": "2023", + "Country": "Italy", + "Unemployment (%)": 7.7 + }, + { + "Year": "2000", + "Country": "Japan", + "Unemployment (%)": 4.7 + }, + { + "Year": "2007", + "Country": "Japan", + "Unemployment (%)": 3.8 + }, + { + "Year": "2010", + "Country": "Japan", + "Unemployment (%)": 5.1 + }, + { + "Year": "2015", + "Country": "Japan", + "Unemployment (%)": 3.4 + }, + { + "Year": "2023", + "Country": "Japan", + "Unemployment (%)": 2.6 + }, + { + "Year": "2000", + "Country": "Netherlands", + "Unemployment (%)": 3.1 + }, + { + "Year": "2007", + "Country": "Netherlands", + "Unemployment (%)": 3.6 + }, + { + "Year": "2010", + "Country": "Netherlands", + "Unemployment (%)": 5 + }, + { + "Year": "2015", + "Country": "Netherlands", + "Unemployment (%)": 6.9 + }, + { + "Year": "2023", + "Country": "Netherlands", + "Unemployment (%)": 3.6 + }, + { + "Year": "2000", + "Country": "Norway", + "Unemployment (%)": 3.2 + }, + { + "Year": "2007", + "Country": "Norway", + "Unemployment (%)": 2.5 + }, + { + "Year": "2010", + "Country": "Norway", + "Unemployment (%)": 3.6 + }, + { + "Year": "2015", + "Country": "Norway", + "Unemployment (%)": 4.4 + }, + { + "Year": "2023", + "Country": "Norway", + "Unemployment (%)": 3.6 + }, + { + "Year": "2000", + "Country": "Spain", + "Unemployment (%)": 13.9 + }, + { + "Year": "2007", + "Country": "Spain", + "Unemployment (%)": 8.2 + }, + { + "Year": "2010", + "Country": "Spain", + "Unemployment (%)": 19.9 + }, + { + "Year": "2015", + "Country": "Spain", + "Unemployment (%)": 22.1 + }, + { + "Year": "2023", + "Country": "Spain", + "Unemployment (%)": 12.2 + }, + { + "Year": "2000", + "Country": "Sweden", + "Unemployment (%)": 5.6 + }, + { + "Year": "2007", + "Country": "Sweden", + "Unemployment (%)": 6.1 + }, + { + "Year": "2010", + "Country": "Sweden", + "Unemployment (%)": 8.6 + }, + { + "Year": "2015", + "Country": "Sweden", + "Unemployment (%)": 7.4 + }, + { + "Year": "2023", + "Country": "Sweden", + "Unemployment (%)": 7.7 + }, + { + "Year": "2000", + "Country": "United States", + "Unemployment (%)": 4 + }, + { + "Year": "2007", + "Country": "United States", + "Unemployment (%)": 4.6 + }, + { + "Year": "2010", + "Country": "United States", + "Unemployment (%)": 9.6 + }, + { + "Year": "2015", + "Country": "United States", + "Unemployment (%)": 5.3 + }, + { + "Year": "2023", + "Country": "United States", + "Unemployment (%)": 3.6 + } + ] + }, + "title": { + "text": "Sixteen labour markets, one shock", + "subtitle": [ + "Harmonised unemployment rate, selected OECD economies, 2000–2023, per cent" + ] + } +} diff --git a/site/src/playground/theme-lab-assets/oecd-facet-16.powerbi.json b/site/src/playground/theme-lab-assets/oecd-facet-16.powerbi.json new file mode 100644 index 00000000..0c3b49a6 --- /dev/null +++ b/site/src/playground/theme-lab-assets/oecd-facet-16.powerbi.json @@ -0,0 +1,193 @@ +{ + "$schema": "https://vega-lite.github.io/schema/vega-lite/v5.json", + "__theme__": "powerbi", + "__design__": [ + "Sixteen panels laid out 4×4 instead of 6×3. Flint's wrap leaves two empty cells in the bottom row, and an empty cell in a grid is read as missing data rather than as spare space. A square grid divides sixteen exactly.", + "Panel frames removed. At this density the sixteen rectangles are the loudest thing on the page — thirty-two vertical rules competing with sixteen data lines. The panels are separated by whitespace and their headers instead.", + "The 'Country' super-header deleted. It titles a strip of headers that are already obviously country names, and it costs a full row of vertical space that sixteen panels cannot spare.", + "Panel order left exactly as the baseline has it. Reordering the grid by rate would help it read as a ranking, but which country follows which is a claim about the data, not a house style — it belongs in the encoding, upstream of any theme.", + "Shared y scale kept, deliberately. It is the reason twelve of the sixteen lines sit low in their panels — but a small multiple exists to be compared across panels, and per-panel scales would make Japan's 2.6% and Spain's 22.1% draw as the same picture. The cost is real and it is the right cost.", + "Y axis labelled once per row and x once per column, at 8pt, with the axis titles dropped — the subtitle carries the unit and the years, so sixteen repetitions of 'Unemployment (%)' state it fifteen times too often.", + "Country names given no label limit; Flint clips 'United Stat…' in a header that has a whole panel width available to it.", + "Dark #1B1A19 report canvas, 1.4px lines in #4FC3F7, no gridlines except a single hairline at each labelled tick." + ], + "background": "#1b1a19", + "padding": { "left": 6, "top": 6, "right": 10, "bottom": 6 }, + "title": { + "text": "Sixteen labour markets, one shock", + "subtitle": ["Harmonised unemployment rate, selected OECD economies, 2000–2023, per cent"] + }, + "data": { + "values": [ + { "Year": 2000, "Country": "Australia", "Unemployment (%)": 6.3 }, + { "Year": 2007, "Country": "Australia", "Unemployment (%)": 4.4 }, + { "Year": 2010, "Country": "Australia", "Unemployment (%)": 5.2 }, + { "Year": 2015, "Country": "Australia", "Unemployment (%)": 6.1 }, + { "Year": 2023, "Country": "Australia", "Unemployment (%)": 3.7 }, + { "Year": 2000, "Country": "Austria", "Unemployment (%)": 4.7 }, + { "Year": 2007, "Country": "Austria", "Unemployment (%)": 4.9 }, + { "Year": 2010, "Country": "Austria", "Unemployment (%)": 4.8 }, + { "Year": 2015, "Country": "Austria", "Unemployment (%)": 5.7 }, + { "Year": 2023, "Country": "Austria", "Unemployment (%)": 5.1 }, + { "Year": 2000, "Country": "Belgium", "Unemployment (%)": 6.9 }, + { "Year": 2007, "Country": "Belgium", "Unemployment (%)": 7.5 }, + { "Year": 2010, "Country": "Belgium", "Unemployment (%)": 8.3 }, + { "Year": 2015, "Country": "Belgium", "Unemployment (%)": 8.5 }, + { "Year": 2023, "Country": "Belgium", "Unemployment (%)": 5.5 }, + { "Year": 2000, "Country": "Canada", "Unemployment (%)": 6.8 }, + { "Year": 2007, "Country": "Canada", "Unemployment (%)": 6 }, + { "Year": 2010, "Country": "Canada", "Unemployment (%)": 8 }, + { "Year": 2015, "Country": "Canada", "Unemployment (%)": 6.9 }, + { "Year": 2023, "Country": "Canada", "Unemployment (%)": 5.4 }, + { "Year": 2000, "Country": "Denmark", "Unemployment (%)": 4.3 }, + { "Year": 2007, "Country": "Denmark", "Unemployment (%)": 3.8 }, + { "Year": 2010, "Country": "Denmark", "Unemployment (%)": 7.5 }, + { "Year": 2015, "Country": "Denmark", "Unemployment (%)": 6.2 }, + { "Year": 2023, "Country": "Denmark", "Unemployment (%)": 5.1 }, + { "Year": 2000, "Country": "Finland", "Unemployment (%)": 9.8 }, + { "Year": 2007, "Country": "Finland", "Unemployment (%)": 6.9 }, + { "Year": 2010, "Country": "Finland", "Unemployment (%)": 8.4 }, + { "Year": 2015, "Country": "Finland", "Unemployment (%)": 9.4 }, + { "Year": 2023, "Country": "Finland", "Unemployment (%)": 7.2 }, + { "Year": 2000, "Country": "France", "Unemployment (%)": 9 }, + { "Year": 2007, "Country": "France", "Unemployment (%)": 8 }, + { "Year": 2010, "Country": "France", "Unemployment (%)": 9.3 }, + { "Year": 2015, "Country": "France", "Unemployment (%)": 10.4 }, + { "Year": 2023, "Country": "France", "Unemployment (%)": 7.3 }, + { "Year": 2000, "Country": "Germany", "Unemployment (%)": 7.9 }, + { "Year": 2007, "Country": "Germany", "Unemployment (%)": 8.7 }, + { "Year": 2010, "Country": "Germany", "Unemployment (%)": 7 }, + { "Year": 2015, "Country": "Germany", "Unemployment (%)": 4.6 }, + { "Year": 2023, "Country": "Germany", "Unemployment (%)": 3.1 }, + { "Year": 2000, "Country": "Ireland", "Unemployment (%)": 4.2 }, + { "Year": 2007, "Country": "Ireland", "Unemployment (%)": 4.7 }, + { "Year": 2010, "Country": "Ireland", "Unemployment (%)": 13.9 }, + { "Year": 2015, "Country": "Ireland", "Unemployment (%)": 9.9 }, + { "Year": 2023, "Country": "Ireland", "Unemployment (%)": 4.3 }, + { "Year": 2000, "Country": "Italy", "Unemployment (%)": 10.1 }, + { "Year": 2007, "Country": "Italy", "Unemployment (%)": 6.1 }, + { "Year": 2010, "Country": "Italy", "Unemployment (%)": 8.4 }, + { "Year": 2015, "Country": "Italy", "Unemployment (%)": 11.9 }, + { "Year": 2023, "Country": "Italy", "Unemployment (%)": 7.7 }, + { "Year": 2000, "Country": "Japan", "Unemployment (%)": 4.7 }, + { "Year": 2007, "Country": "Japan", "Unemployment (%)": 3.8 }, + { "Year": 2010, "Country": "Japan", "Unemployment (%)": 5.1 }, + { "Year": 2015, "Country": "Japan", "Unemployment (%)": 3.4 }, + { "Year": 2023, "Country": "Japan", "Unemployment (%)": 2.6 }, + { "Year": 2000, "Country": "Netherlands", "Unemployment (%)": 3.1 }, + { "Year": 2007, "Country": "Netherlands", "Unemployment (%)": 3.6 }, + { "Year": 2010, "Country": "Netherlands", "Unemployment (%)": 5 }, + { "Year": 2015, "Country": "Netherlands", "Unemployment (%)": 6.9 }, + { "Year": 2023, "Country": "Netherlands", "Unemployment (%)": 3.6 }, + { "Year": 2000, "Country": "Norway", "Unemployment (%)": 3.2 }, + { "Year": 2007, "Country": "Norway", "Unemployment (%)": 2.5 }, + { "Year": 2010, "Country": "Norway", "Unemployment (%)": 3.6 }, + { "Year": 2015, "Country": "Norway", "Unemployment (%)": 4.4 }, + { "Year": 2023, "Country": "Norway", "Unemployment (%)": 3.6 }, + { "Year": 2000, "Country": "Spain", "Unemployment (%)": 13.9 }, + { "Year": 2007, "Country": "Spain", "Unemployment (%)": 8.2 }, + { "Year": 2010, "Country": "Spain", "Unemployment (%)": 19.9 }, + { "Year": 2015, "Country": "Spain", "Unemployment (%)": 22.1 }, + { "Year": 2023, "Country": "Spain", "Unemployment (%)": 12.2 }, + { "Year": 2000, "Country": "Sweden", "Unemployment (%)": 5.6 }, + { "Year": 2007, "Country": "Sweden", "Unemployment (%)": 6.1 }, + { "Year": 2010, "Country": "Sweden", "Unemployment (%)": 8.6 }, + { "Year": 2015, "Country": "Sweden", "Unemployment (%)": 7.4 }, + { "Year": 2023, "Country": "Sweden", "Unemployment (%)": 7.7 }, + { "Year": 2000, "Country": "United States", "Unemployment (%)": 4 }, + { "Year": 2007, "Country": "United States", "Unemployment (%)": 4.6 }, + { "Year": 2010, "Country": "United States", "Unemployment (%)": 9.6 }, + { "Year": 2015, "Country": "United States", "Unemployment (%)": 5.3 }, + { "Year": 2023, "Country": "United States", "Unemployment (%)": 3.6 } + ] + }, + "facet": { + "field": "Country", + "type": "nominal", + "title": null, + "sort": null, + "header": { + "labelAngle": 0, + "labelAlign": "left", + "labelAnchor": "start", + "labelFontSize": 9.5, + "labelColor": "#f3f2f1", + "labelFontWeight": 600, + "labelLimit": 0, + "labelPadding": 2, + "titleColor": "#a19f9d" + } + }, + "columns": 4, + "spacing": { "row": 16, "column": 16 }, + "spec": { + "width": 110, + "height": 62, + "mark": { "type": "line", "color": "#4fc3f7", "strokeWidth": 1.4, "strokeCap": "round" }, + "encoding": { + "x": { + "field": "Year", + "type": "quantitative", + "title": null, + "scale": { "zero": false, "nice": false, "domain": [2000, 2023] }, + "axis": { + "values": [2000, 2010, 2023], + "format": "d", + "labelFontSize": 8, + "labelColor": "#8a8886", + "grid": false, + "domain": false, + "ticks": false, + "labelPadding": 3 + } + }, + "y": { + "field": "Unemployment (%)", + "type": "quantitative", + "title": null, + "scale": { "zero": true, "nice": false, "domain": [0, 24] }, + "axis": { + "values": [0, 10, 20], + "format": "d", + "labelFontSize": 8, + "labelColor": "#8a8886", + "grid": true, + "gridColor": "#302f2d", + "gridWidth": 1, + "domain": false, + "ticks": false, + "labelPadding": 3 + } + } + } + }, + "config": { + "background": "#1b1a19", + "font": "'Segoe UI', 'Segoe UI Variable', system-ui, -apple-system, sans-serif", + "title": { + "font": "'Segoe UI', 'Segoe UI Variable', system-ui, sans-serif", + "fontSize": 12.5, + "fontWeight": 600, + "color": "#f3f2f1", + "anchor": "start", + "offset": 10, + "subtitleFont": "'Segoe UI', 'Segoe UI Variable', system-ui, sans-serif", + "subtitleFontSize": 10, + "subtitleColor": "#a19f9d", + "subtitlePadding": 5 + }, + "view": { "stroke": null }, + "axis": { + "labelFont": "'Segoe UI', 'Segoe UI Variable', system-ui, sans-serif", + "labelFontSize": 8, + "labelColor": "#8a8886", + "domain": false, + "ticks": false, + "grid": false + }, + "header": { + "labelFont": "'Segoe UI', 'Segoe UI Variable', system-ui, sans-serif", + "titleFont": "'Segoe UI', 'Segoe UI Variable', system-ui, sans-serif" + } + } +} diff --git a/site/src/playground/theme-lab-assets/oecd-unemployment-facet.economist.json b/site/src/playground/theme-lab-assets/oecd-unemployment-facet.economist.json new file mode 100644 index 00000000..723c58ed --- /dev/null +++ b/site/src/playground/theme-lab-assets/oecd-unemployment-facet.economist.json @@ -0,0 +1,138 @@ +{ + "$schema": "https://vega-lite.github.io/schema/vega-lite/v5.json", + "__theme__": "economist", + "__design__": [ + "Masthead rule added above the panels as its own vconcat row — the same brand furniture the paper puts on every chart, carrying no data.", + "Panel headers rewritten as flush-left bold labels with no 'Country' title and no boxes. Flint emits a centred grey header per facet; the house style treats each panel title as a small headline.", + "Shared y axis moved to the right of the last panel is not expressible in Vega-Lite, so the compromise is: y labels only on the first panel (via a per-panel axis config) and gridlines carried across all four.", + "Y scale pinned to a common [0, 24] domain. Flint already shares the scale, but pinning it makes the comparison a stated property rather than a side effect of the data.", + "Panel geometry widened to 88×150 with 14px spacing — four narrow columns read as one chart; Flint's 105×155 with 11px spacing reads as four charts.", + "Type dropped to 8.5pt labels and the x axis reduced to two ticks per panel (start and end), because a small multiple is read by shape, not by value.", + "House blue #006BA2 at 1.6px replaces the default hairline; the tick marks and baseline go hard black." + ], + "background": "#ffffff", + "padding": { "left": 6, "top": 4, "right": 10, "bottom": 6 }, + "title": { + "text": "Out of work", + "subtitle": ["Unemployment rate, %, 2000–2022"] + }, + "spacing": 10, + "vconcat": [ + { + "data": { "values": [{}] }, + "mark": { "type": "rect", "fill": "#e3120b", "stroke": null }, + "width": 26, + "height": 3, + "view": { "stroke": null } + }, + { + "data": { + "values": [ + { "Year": "2000", "Country": "United States", "Unemployment (%)": 4.0 }, + { "Year": "2005", "Country": "United States", "Unemployment (%)": 5.1 }, + { "Year": "2010", "Country": "United States", "Unemployment (%)": 9.6 }, + { "Year": "2015", "Country": "United States", "Unemployment (%)": 5.3 }, + { "Year": "2020", "Country": "United States", "Unemployment (%)": 8.1 }, + { "Year": "2022", "Country": "United States", "Unemployment (%)": 3.6 }, + { "Year": "2000", "Country": "Germany", "Unemployment (%)": 7.9 }, + { "Year": "2005", "Country": "Germany", "Unemployment (%)": 11.2 }, + { "Year": "2010", "Country": "Germany", "Unemployment (%)": 7.0 }, + { "Year": "2015", "Country": "Germany", "Unemployment (%)": 4.6 }, + { "Year": "2020", "Country": "Germany", "Unemployment (%)": 3.7 }, + { "Year": "2022", "Country": "Germany", "Unemployment (%)": 3.1 }, + { "Year": "2000", "Country": "Japan", "Unemployment (%)": 4.7 }, + { "Year": "2005", "Country": "Japan", "Unemployment (%)": 4.4 }, + { "Year": "2010", "Country": "Japan", "Unemployment (%)": 5.1 }, + { "Year": "2015", "Country": "Japan", "Unemployment (%)": 3.4 }, + { "Year": "2020", "Country": "Japan", "Unemployment (%)": 2.8 }, + { "Year": "2022", "Country": "Japan", "Unemployment (%)": 2.6 }, + { "Year": "2000", "Country": "Spain", "Unemployment (%)": 13.9 }, + { "Year": "2005", "Country": "Spain", "Unemployment (%)": 9.2 }, + { "Year": "2010", "Country": "Spain", "Unemployment (%)": 19.9 }, + { "Year": "2015", "Country": "Spain", "Unemployment (%)": 22.1 }, + { "Year": "2020", "Country": "Spain", "Unemployment (%)": 15.5 }, + { "Year": "2022", "Country": "Spain", "Unemployment (%)": 12.9 } + ] + }, + "facet": { + "field": "Country", + "type": "nominal", + "sort": ["United States", "Germany", "Japan", "Spain"], + "columns": 4, + "title": null, + "header": { + "labelAnchor": "start", + "labelFontSize": 10, + "labelFontWeight": 700, + "labelColor": "#121317", + "labelPadding": 2, + "labelOrient": "top" + } + }, + "spacing": 14, + "spec": { + "width": 88, + "height": 150, + "mark": { "type": "line", "color": "#006ba2", "strokeWidth": 1.6 }, + "encoding": { + "x": { + "field": "Year", + "type": "temporal", + "title": null, + "scale": { "type": "utc" }, + "axis": { + "format": "%y", + "values": ["2000", "2022"], + "labelFlush": true, + "grid": false, + "domain": true, + "domainColor": "#121317", + "ticks": true, + "tickColor": "#121317", + "tickSize": 3 + } + }, + "y": { + "field": "Unemployment (%)", + "type": "quantitative", + "title": null, + "scale": { "domain": [0, 24] }, + "axis": { + "tickCount": 4, + "grid": true, + "gridColor": "#c9d3da", + "domain": false, + "ticks": false, + "labelPadding": 3 + } + } + } + }, + "resolve": { "axis": { "y": "shared" } } + } + ], + "config": { + "background": "#ffffff", + "font": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "title": { + "font": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "fontSize": 13.5, + "fontWeight": 700, + "color": "#121317", + "anchor": "start", + "offset": 10, + "subtitleFont": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "subtitleFontSize": 10, + "subtitleColor": "#54585a", + "subtitlePadding": 6 + }, + "view": { "stroke": null }, + "axis": { + "labelFont": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "labelFontSize": 8.5, + "labelColor": "#54585a", + "titleFontSize": 9 + }, + "header": { "labelFont": "'Helvetica Neue', Helvetica, Arial, sans-serif" } + } +} diff --git a/site/src/playground/theme-lab-assets/oecd-unemployment-facet.flint.json b/site/src/playground/theme-lab-assets/oecd-unemployment-facet.flint.json new file mode 100644 index 00000000..5efd358a --- /dev/null +++ b/site/src/playground/theme-lab-assets/oecd-unemployment-facet.flint.json @@ -0,0 +1,184 @@ +{ + "mark": "line", + "encoding": { + "x": { + "field": "Year", + "type": "temporal" + }, + "y": { + "field": "Unemployment (%)", + "type": "quantitative", + "scale": { + "zero": true + }, + "axis": { + "format": ",.12~g" + } + }, + "facet": { + "field": "Country", + "type": "nominal", + "sort": null, + "columns": 4 + } + }, + "config": { + "view": { + "continuousWidth": 105, + "continuousHeight": 155 + }, + "axisX": { + "labelLimit": 100, + "labelFontSize": 8, + "titleFontSize": 11, + "titleFontWeight": "normal", + "titleColor": "#666" + }, + "axisY": { + "labelFontSize": 8, + "titleFontSize": 11, + "titleFontWeight": "normal", + "titleColor": "#666" + }, + "legend": { + "labelFontSize": 9, + "titleFontSize": 9 + }, + "headerFacet": { + "labelLimit": 125 + }, + "facet": { + "spacing": 11 + } + }, + "data": { + "values": [ + { + "Year": "2000", + "Country": "United States", + "Unemployment (%)": 4 + }, + { + "Year": "2005", + "Country": "United States", + "Unemployment (%)": 5.1 + }, + { + "Year": "2010", + "Country": "United States", + "Unemployment (%)": 9.6 + }, + { + "Year": "2015", + "Country": "United States", + "Unemployment (%)": 5.3 + }, + { + "Year": "2020", + "Country": "United States", + "Unemployment (%)": 8.1 + }, + { + "Year": "2022", + "Country": "United States", + "Unemployment (%)": 3.6 + }, + { + "Year": "2000", + "Country": "Germany", + "Unemployment (%)": 7.9 + }, + { + "Year": "2005", + "Country": "Germany", + "Unemployment (%)": 11.2 + }, + { + "Year": "2010", + "Country": "Germany", + "Unemployment (%)": 7 + }, + { + "Year": "2015", + "Country": "Germany", + "Unemployment (%)": 4.6 + }, + { + "Year": "2020", + "Country": "Germany", + "Unemployment (%)": 3.7 + }, + { + "Year": "2022", + "Country": "Germany", + "Unemployment (%)": 3.1 + }, + { + "Year": "2000", + "Country": "Japan", + "Unemployment (%)": 4.7 + }, + { + "Year": "2005", + "Country": "Japan", + "Unemployment (%)": 4.4 + }, + { + "Year": "2010", + "Country": "Japan", + "Unemployment (%)": 5.1 + }, + { + "Year": "2015", + "Country": "Japan", + "Unemployment (%)": 3.4 + }, + { + "Year": "2020", + "Country": "Japan", + "Unemployment (%)": 2.8 + }, + { + "Year": "2022", + "Country": "Japan", + "Unemployment (%)": 2.6 + }, + { + "Year": "2000", + "Country": "Spain", + "Unemployment (%)": 13.9 + }, + { + "Year": "2005", + "Country": "Spain", + "Unemployment (%)": 9.2 + }, + { + "Year": "2010", + "Country": "Spain", + "Unemployment (%)": 19.9 + }, + { + "Year": "2015", + "Country": "Spain", + "Unemployment (%)": 22.1 + }, + { + "Year": "2020", + "Country": "Spain", + "Unemployment (%)": 15.5 + }, + { + "Year": "2022", + "Country": "Spain", + "Unemployment (%)": 12.9 + } + ] + }, + "title": { + "text": "Out of work", + "subtitle": [ + "Unemployment rate, %, 2000–2022" + ] + } +} diff --git a/site/src/playground/theme-lab-assets/olympic-bump.flint.json b/site/src/playground/theme-lab-assets/olympic-bump.flint.json new file mode 100644 index 00000000..f3cad6df --- /dev/null +++ b/site/src/playground/theme-lab-assets/olympic-bump.flint.json @@ -0,0 +1,145 @@ +{ + "mark": { + "type": "line", + "point": true, + "interpolate": "monotone", + "strokeWidth": 2 + }, + "encoding": { + "x": { + "field": "Games", + "type": "temporal" + }, + "y": { + "field": "Rank", + "type": "quantitative", + "scale": { + "reverse": true, + "zero": true + }, + "axis": { + "format": ",.12~g" + } + }, + "color": { + "field": "Country", + "type": "nominal", + "sort": null, + "scale": { + "scheme": "set2" + } + } + }, + "config": { + "view": { + "continuousWidth": 308, + "continuousHeight": 254 + }, + "axisX": { + "labelLimit": 100, + "labelFontSize": 10, + "titleFontSize": 11 + }, + "axisY": { + "labelFontSize": 10, + "titleFontSize": 11 + }, + "legend": { + "labelFontSize": 10, + "titleFontSize": 11 + }, + "facet": { + "spacing": 25 + } + }, + "data": { + "values": [ + { + "Games": "2012", + "Country": "United States", + "Rank": 1 + }, + { + "Games": "2016", + "Country": "United States", + "Rank": 1 + }, + { + "Games": "2020", + "Country": "United States", + "Rank": 1 + }, + { + "Games": "2024", + "Country": "United States", + "Rank": 1 + }, + { + "Games": "2012", + "Country": "China", + "Rank": 2 + }, + { + "Games": "2016", + "Country": "China", + "Rank": 3 + }, + { + "Games": "2020", + "Country": "China", + "Rank": 2 + }, + { + "Games": "2024", + "Country": "China", + "Rank": 2 + }, + { + "Games": "2012", + "Country": "Great Britain", + "Rank": 3 + }, + { + "Games": "2016", + "Country": "Great Britain", + "Rank": 2 + }, + { + "Games": "2020", + "Country": "Great Britain", + "Rank": 4 + }, + { + "Games": "2024", + "Country": "Great Britain", + "Rank": 7 + }, + { + "Games": "2012", + "Country": "Japan", + "Rank": 6 + }, + { + "Games": "2016", + "Country": "Japan", + "Rank": 6 + }, + { + "Games": "2020", + "Country": "Japan", + "Rank": 5 + }, + { + "Games": "2024", + "Country": "Japan", + "Rank": 3 + } + ] + }, + "title": { + "text": "Four Games, four different stories", + "subtitle": [ + "Rank in the Summer Olympics medal table, 2012–2024" + ] + } +} diff --git a/site/src/playground/theme-lab-assets/olympic-bump.nyt.json b/site/src/playground/theme-lab-assets/olympic-bump.nyt.json new file mode 100644 index 00000000..cf307207 --- /dev/null +++ b/site/src/playground/theme-lab-assets/olympic-bump.nyt.json @@ -0,0 +1,134 @@ +{ + "$schema": "https://vega-lite.github.io/schema/vega-lite/v5.json", + "__theme__": "nyt", + "__design__": [ + "Legend replaced by a direct label at the end of every line. On a bump chart the legend is the worst possible key — it forces a colour lookup for each of four lines at each of four columns, when the country name can simply sit where the line ends. Labels go on the right only; a matching set on the left collided with the rank axis, and the axis already states where each line starts.", + "Rank axis relabelled as ordinals (1st, 2nd, …) instead of the bare integers Flint prints. The numbers are ranks, not a quantity, and 'zero: true' on a rank scale — which the baseline sets — reserves space below 1st for a rank that cannot exist.", + "Tick set pinned to the ranks actually occupied (1–7) so no line lands between gridlines, and the axis title deleted: once the labels read '1st', the word 'rank' is redundant.", + "Palette cut from set2 to four NYT swatches used at full ink for the two constant leaders and desaturated grey for none of them — every series is a real answer to 'who finished where', so nothing here earns being greyed out.", + "Monotone interpolation dropped for straight segments. A bump chart's slope is its message; a spline invents curvature between two Games where nothing was measured.", + "Frame removed, gridlines turned horizontal-only and pushed to #ededed, and the x axis reduced to four Games labels on a black baseline." + ], + "background": "#ffffff", + "padding": { "left": 8, "top": 6, "right": 8, "bottom": 6 }, + "width": 210, + "height": 200, + "title": { + "text": "Four Games, four different stories", + "subtitle": ["Rank in the Summer Olympics medal table, 2012–2024"] + }, + "data": { + "values": [ + { "Games": "2012", "Country": "United States", "Rank": 1 }, + { "Games": "2016", "Country": "United States", "Rank": 1 }, + { "Games": "2020", "Country": "United States", "Rank": 1 }, + { "Games": "2024", "Country": "United States", "Rank": 1 }, + { "Games": "2012", "Country": "China", "Rank": 2 }, + { "Games": "2016", "Country": "China", "Rank": 3 }, + { "Games": "2020", "Country": "China", "Rank": 2 }, + { "Games": "2024", "Country": "China", "Rank": 2 }, + { "Games": "2012", "Country": "Great Britain", "Rank": 3 }, + { "Games": "2016", "Country": "Great Britain", "Rank": 2 }, + { "Games": "2020", "Country": "Great Britain", "Rank": 4 }, + { "Games": "2024", "Country": "Great Britain", "Rank": 7 }, + { "Games": "2012", "Country": "Japan", "Rank": 6 }, + { "Games": "2016", "Country": "Japan", "Rank": 6 }, + { "Games": "2020", "Country": "Japan", "Rank": 5 }, + { "Games": "2024", "Country": "Japan", "Rank": 3 } + ] + }, + "encoding": { + "x": { + "field": "Games", + "type": "ordinal", + "title": null, + "scale": { "padding": 0.12 }, + "axis": { "labelAngle": 0, "labelPadding": 6 } + }, + "y": { + "field": "Rank", + "type": "quantitative", + "title": null, + "scale": { "reverse": true, "zero": false, "nice": false, "domain": [1, 7], "padding": 14 }, + "axis": { + "values": [1, 2, 3, 4, 5, 6, 7], + "labelExpr": "datum.value == 1 ? '1st' : datum.value == 2 ? '2nd' : datum.value == 3 ? '3rd' : datum.value + 'th'" + } + }, + "color": { + "field": "Country", + "type": "nominal", + "legend": null, + "scale": { + "domain": ["United States", "China", "Great Britain", "Japan"], + "range": ["#c2352b", "#2f6b9a", "#7f6a9e", "#4a8b6f"] + } + } + }, + "layer": [ + { "mark": { "type": "line", "strokeWidth": 2, "strokeJoin": "round", "strokeCap": "round" } }, + { + "mark": { + "type": "point", + "filled": true, + "size": 34, + "stroke": "#ffffff", + "strokeWidth": 1.4 + } + }, + { + "transform": [{ "filter": "datum.Games === '2024'" }], + "mark": { + "type": "text", + "align": "left", + "baseline": "middle", + "dx": 8, + "fontSize": 10, + "fontWeight": 600 + }, + "encoding": { "text": { "field": "Country", "type": "nominal" } } + } + ], + "config": { + "background": "#ffffff", + "font": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "title": { + "font": "Georgia, 'Times New Roman', Times, serif", + "fontSize": 15, + "fontWeight": 700, + "color": "#121212", + "anchor": "start", + "offset": 14, + "subtitleFont": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "subtitleFontSize": 10.5, + "subtitleColor": "#6b6b6b", + "subtitlePadding": 7 + }, + "view": { "stroke": null }, + "axis": { + "labelFont": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "labelFontSize": 10, + "labelColor": "#6b6b6b", + "domain": false, + "ticks": false, + "grid": false + }, + "axisY": { + "grid": true, + "gridColor": "#ededed", + "gridWidth": 1, + "labelPadding": 6, + "labelFontWeight": 600, + "labelColor": "#121212" + }, + "axisX": { + "grid": false, + "domain": true, + "domainColor": "#121212", + "domainWidth": 1, + "ticks": true, + "tickColor": "#121212", + "tickSize": 4 + } + } +} diff --git a/site/src/playground/theme-lab-assets/penguins-box.flint.json b/site/src/playground/theme-lab-assets/penguins-box.flint.json new file mode 100644 index 00000000..2831c773 --- /dev/null +++ b/site/src/playground/theme-lab-assets/penguins-box.flint.json @@ -0,0 +1,190 @@ +{ + "mark": { + "type": "boxplot", + "size": 22 + }, + "encoding": { + "x": { + "field": "Species", + "type": "nominal", + "sort": null + }, + "y": { + "field": "Body mass (g)", + "type": "quantitative", + "scale": { + "zero": true + }, + "axis": { + "format": ",.12~g" + } + } + }, + "config": { + "view": { + "continuousWidth": 340, + "continuousHeight": 230 + }, + "axisX": { + "labelLimit": 100, + "labelFontSize": 10, + "titleFontSize": 11 + }, + "axisY": { + "labelFontSize": 10, + "titleFontSize": 11 + }, + "legend": { + "labelFontSize": 10, + "titleFontSize": 11 + }, + "facet": { + "spacing": 23 + } + }, + "width": { + "step": 32 + }, + "data": { + "values": [ + { + "Species": "Adelie", + "Body mass (g)": 3750 + }, + { + "Species": "Adelie", + "Body mass (g)": 3800 + }, + { + "Species": "Adelie", + "Body mass (g)": 3250 + }, + { + "Species": "Adelie", + "Body mass (g)": 3450 + }, + { + "Species": "Adelie", + "Body mass (g)": 3650 + }, + { + "Species": "Adelie", + "Body mass (g)": 3625 + }, + { + "Species": "Adelie", + "Body mass (g)": 4675 + }, + { + "Species": "Adelie", + "Body mass (g)": 3200 + }, + { + "Species": "Adelie", + "Body mass (g)": 3800 + }, + { + "Species": "Adelie", + "Body mass (g)": 4400 + }, + { + "Species": "Adelie", + "Body mass (g)": 3700 + }, + { + "Species": "Chinstrap", + "Body mass (g)": 3500 + }, + { + "Species": "Chinstrap", + "Body mass (g)": 3900 + }, + { + "Species": "Chinstrap", + "Body mass (g)": 3650 + }, + { + "Species": "Chinstrap", + "Body mass (g)": 3525 + }, + { + "Species": "Chinstrap", + "Body mass (g)": 3950 + }, + { + "Species": "Chinstrap", + "Body mass (g)": 3800 + }, + { + "Species": "Chinstrap", + "Body mass (g)": 3300 + }, + { + "Species": "Chinstrap", + "Body mass (g)": 4800 + }, + { + "Species": "Chinstrap", + "Body mass (g)": 4050 + }, + { + "Species": "Chinstrap", + "Body mass (g)": 3550 + }, + { + "Species": "Gentoo", + "Body mass (g)": 4500 + }, + { + "Species": "Gentoo", + "Body mass (g)": 5700 + }, + { + "Species": "Gentoo", + "Body mass (g)": 4450 + }, + { + "Species": "Gentoo", + "Body mass (g)": 5700 + }, + { + "Species": "Gentoo", + "Body mass (g)": 5400 + }, + { + "Species": "Gentoo", + "Body mass (g)": 5550 + }, + { + "Species": "Gentoo", + "Body mass (g)": 4800 + }, + { + "Species": "Gentoo", + "Body mass (g)": 5000 + }, + { + "Species": "Gentoo", + "Body mass (g)": 4650 + }, + { + "Species": "Gentoo", + "Body mass (g)": 5550 + }, + { + "Species": "Gentoo", + "Body mass (g)": 5950 + }, + { + "Species": "Gentoo", + "Body mass (g)": 5250 + } + ] + }, + "title": { + "text": "Body mass of three Pygoscelis species", + "subtitle": [ + "Boxes show median and interquartile range; whiskers span the full sample; points are individual birds" + ] + } +} diff --git a/site/src/playground/theme-lab-assets/penguins-box.nature.json b/site/src/playground/theme-lab-assets/penguins-box.nature.json new file mode 100644 index 00000000..c426c7f0 --- /dev/null +++ b/site/src/playground/theme-lab-assets/penguins-box.nature.json @@ -0,0 +1,165 @@ +{ + "$schema": "https://vega-lite.github.io/schema/vega-lite/v5.json", + "__theme__": "nature", + "__design__": [ + "Box replaced by a box-and-points composite: the summary box is drawn narrow and hollow, and every observation is jittered across it so the points read through the outline. At n = 10–13 per group a filled box hides the sample it claims to summarise; journals require the reader to see n.", + "Whiskers set to the full data range (extent 'min-max') rather than 1.5×IQR, because with this few points the Tukey fence produces outliers that are not outliers.", + "Box fill removed and the outline taken to 1px black; the median becomes the only heavy rule in the mark, which is the single value the box exists to communicate.", + "Colour moved off the box and onto the observation dots, using the Okabe–Ito colourblind-safe triple (#0072B2, #E69F00, #009E73) rather than tableau10.", + "Legend deleted — the species are already labelled on the x axis, so a colour key restates the axis.", + "Grid removed and replaced with black L-spines and outward 3.5px ticks; y domain pinned to 3000–6000 so the tick interval is a round 500 g.", + "Y title shortened to 'Body mass (g)' at 9pt and the whole figure sized to a 240×250 single-column block, the width a journal actually prints." + ], + "background": "#ffffff", + "padding": { "left": 4, "top": 4, "right": 6, "bottom": 4 }, + "width": { "step": 74 }, + "height": 250, + "title": { + "text": "Body mass of three Pygoscelis species", + "subtitle": ["Boxes show median and interquartile range; whiskers span the full sample; points are individual birds"] + }, + "data": { + "values": [ + { "Species": "Adelie", "Body mass (g)": 3750 }, + { "Species": "Adelie", "Body mass (g)": 3800 }, + { "Species": "Adelie", "Body mass (g)": 3250 }, + { "Species": "Adelie", "Body mass (g)": 3450 }, + { "Species": "Adelie", "Body mass (g)": 3650 }, + { "Species": "Adelie", "Body mass (g)": 3625 }, + { "Species": "Adelie", "Body mass (g)": 4675 }, + { "Species": "Adelie", "Body mass (g)": 3200 }, + { "Species": "Adelie", "Body mass (g)": 3800 }, + { "Species": "Adelie", "Body mass (g)": 4400 }, + { "Species": "Adelie", "Body mass (g)": 3700 }, + { "Species": "Chinstrap", "Body mass (g)": 3500 }, + { "Species": "Chinstrap", "Body mass (g)": 3900 }, + { "Species": "Chinstrap", "Body mass (g)": 3650 }, + { "Species": "Chinstrap", "Body mass (g)": 3525 }, + { "Species": "Chinstrap", "Body mass (g)": 3950 }, + { "Species": "Chinstrap", "Body mass (g)": 3800 }, + { "Species": "Chinstrap", "Body mass (g)": 3300 }, + { "Species": "Chinstrap", "Body mass (g)": 4800 }, + { "Species": "Chinstrap", "Body mass (g)": 4050 }, + { "Species": "Chinstrap", "Body mass (g)": 3550 }, + { "Species": "Gentoo", "Body mass (g)": 4500 }, + { "Species": "Gentoo", "Body mass (g)": 5700 }, + { "Species": "Gentoo", "Body mass (g)": 4450 }, + { "Species": "Gentoo", "Body mass (g)": 5700 }, + { "Species": "Gentoo", "Body mass (g)": 5400 }, + { "Species": "Gentoo", "Body mass (g)": 5550 }, + { "Species": "Gentoo", "Body mass (g)": 4800 }, + { "Species": "Gentoo", "Body mass (g)": 5000 }, + { "Species": "Gentoo", "Body mass (g)": 4650 }, + { "Species": "Gentoo", "Body mass (g)": 5550 }, + { "Species": "Gentoo", "Body mass (g)": 5950 }, + { "Species": "Gentoo", "Body mass (g)": 5250 } + ] + }, + "encoding": { + "x": { + "field": "Species", + "type": "nominal", + "sort": ["Adelie", "Chinstrap", "Gentoo"], + "title": null, + "axis": { + "labelAngle": 0, + "grid": false, + "domain": true, + "domainColor": "#000000", + "domainWidth": 1, + "ticks": true, + "tickColor": "#000000", + "tickSize": 3.5, + "labelPadding": 3 + } + }, + "y": { + "field": "Body mass (g)", + "type": "quantitative", + "title": "Body mass (g)", + "scale": { "domain": [3000, 6000], "nice": false }, + "axis": { + "values": [3000, 3500, 4000, 4500, 5000, 5500, 6000], + "grid": false, + "domain": true, + "domainColor": "#000000", + "domainWidth": 1, + "ticks": true, + "tickColor": "#000000", + "tickSize": 3.5, + "labelPadding": 2, + "titlePadding": 4 + } + } + }, + "layer": [ + { + "mark": { + "type": "boxplot", + "extent": "min-max", + "size": 26, + "box": { "fill": null, "stroke": "#000000", "strokeWidth": 1 }, + "median": { "stroke": "#000000", "strokeWidth": 1.8 }, + "rule": { "stroke": "#000000", "strokeWidth": 1 }, + "ticks": { "stroke": "#000000", "strokeWidth": 1, "size": 9 }, + "outliers": false + } + }, + { + "transform": [ + { "calculate": "random() * 2 - 1", "as": "__jitter" } + ], + "mark": { + "type": "point", + "filled": true, + "size": 22, + "opacity": 0.85, + "stroke": "#ffffff", + "strokeWidth": 0.4 + }, + "encoding": { + "xOffset": { + "field": "__jitter", + "type": "quantitative", + "scale": { "domain": [-3, 3] } + }, + "color": { + "field": "Species", + "type": "nominal", + "scale": { + "domain": ["Adelie", "Chinstrap", "Gentoo"], + "range": ["#0072b2", "#e69f00", "#009e73"] + }, + "legend": null + } + } + } + ], + "config": { + "background": "#ffffff", + "font": "Helvetica, Arial, sans-serif", + "title": { + "font": "Helvetica, Arial, sans-serif", + "fontSize": 10.5, + "fontWeight": 700, + "color": "#000000", + "anchor": "start", + "offset": 10, + "subtitleFont": "Helvetica, Arial, sans-serif", + "subtitleFontSize": 8, + "subtitleColor": "#3c3c3c", + "subtitleLineHeight": 11, + "subtitlePadding": 6 + }, + "view": { "stroke": null }, + "axis": { + "labelFont": "Helvetica, Arial, sans-serif", + "labelFontSize": 8.5, + "labelColor": "#000000", + "titleFont": "Helvetica, Arial, sans-serif", + "titleFontSize": 9, + "titleColor": "#000000", + "titleFontWeight": 400 + } + } +} diff --git a/site/src/playground/theme-lab-assets/penguins-violin.flint.json b/site/src/playground/theme-lab-assets/penguins-violin.flint.json new file mode 100644 index 00000000..64c60389 --- /dev/null +++ b/site/src/playground/theme-lab-assets/penguins-violin.flint.json @@ -0,0 +1,228 @@ +{ + "mark": { + "type": "area", + "orient": "horizontal" + }, + "transform": [ + { + "density": "Body mass (g)", + "groupby": [ + "Species" + ], + "as": [ + "value", + "density" + ], + "extent": [ + 2702.9783569331094, + 6447.021643066891 + ] + } + ], + "encoding": { + "y": { + "field": "value", + "type": "quantitative", + "title": "Body mass (g)", + "axis": { + "format": ",.12~g" + } + }, + "x": { + "field": "density", + "type": "quantitative", + "stack": "center", + "impute": null, + "title": null, + "axis": { + "labels": false, + "ticks": false, + "grid": false, + "format": ",.12~g" + } + }, + "color": { + "field": "Species", + "type": "nominal", + "legend": null + }, + "facet": { + "field": "Species", + "type": "nominal", + "sort": null, + "spacing": 0, + "header": { + "titleOrient": "bottom", + "labelOrient": "bottom", + "labelPadding": 2 + }, + "columns": 3 + } + }, + "width": 93, + "height": 160, + "config": { + "view": { + "continuousWidth": 93, + "continuousHeight": 160 + }, + "axisX": { + "labelLimit": 100, + "labelFontSize": 10, + "titleFontSize": 11 + }, + "axisY": { + "labelFontSize": 10, + "titleFontSize": 11 + }, + "legend": { + "labelFontSize": 10, + "titleFontSize": 11 + }, + "facet": { + "spacing": 23 + } + }, + "data": { + "values": [ + { + "Species": "Adelie", + "Body mass (g)": 3750 + }, + { + "Species": "Adelie", + "Body mass (g)": 3800 + }, + { + "Species": "Adelie", + "Body mass (g)": 3250 + }, + { + "Species": "Adelie", + "Body mass (g)": 3450 + }, + { + "Species": "Adelie", + "Body mass (g)": 3650 + }, + { + "Species": "Adelie", + "Body mass (g)": 3625 + }, + { + "Species": "Adelie", + "Body mass (g)": 4675 + }, + { + "Species": "Adelie", + "Body mass (g)": 3200 + }, + { + "Species": "Adelie", + "Body mass (g)": 3800 + }, + { + "Species": "Adelie", + "Body mass (g)": 4400 + }, + { + "Species": "Adelie", + "Body mass (g)": 3700 + }, + { + "Species": "Chinstrap", + "Body mass (g)": 3500 + }, + { + "Species": "Chinstrap", + "Body mass (g)": 3900 + }, + { + "Species": "Chinstrap", + "Body mass (g)": 3650 + }, + { + "Species": "Chinstrap", + "Body mass (g)": 3525 + }, + { + "Species": "Chinstrap", + "Body mass (g)": 3950 + }, + { + "Species": "Chinstrap", + "Body mass (g)": 3800 + }, + { + "Species": "Chinstrap", + "Body mass (g)": 3300 + }, + { + "Species": "Chinstrap", + "Body mass (g)": 4800 + }, + { + "Species": "Chinstrap", + "Body mass (g)": 4050 + }, + { + "Species": "Chinstrap", + "Body mass (g)": 3550 + }, + { + "Species": "Gentoo", + "Body mass (g)": 4500 + }, + { + "Species": "Gentoo", + "Body mass (g)": 5700 + }, + { + "Species": "Gentoo", + "Body mass (g)": 4450 + }, + { + "Species": "Gentoo", + "Body mass (g)": 5700 + }, + { + "Species": "Gentoo", + "Body mass (g)": 5400 + }, + { + "Species": "Gentoo", + "Body mass (g)": 5550 + }, + { + "Species": "Gentoo", + "Body mass (g)": 4800 + }, + { + "Species": "Gentoo", + "Body mass (g)": 5000 + }, + { + "Species": "Gentoo", + "Body mass (g)": 4650 + }, + { + "Species": "Gentoo", + "Body mass (g)": 5550 + }, + { + "Species": "Gentoo", + "Body mass (g)": 5950 + }, + { + "Species": "Gentoo", + "Body mass (g)": 5250 + } + ] + }, + "title": { + "text": "Body mass by penguin species", + "subtitle": [ + "Kernel density with all observations overlaid; horizontal rule marks the median" + ] + } +} diff --git a/site/src/playground/theme-lab-assets/penguins-violin.nature.json b/site/src/playground/theme-lab-assets/penguins-violin.nature.json new file mode 100644 index 00000000..df629c3e --- /dev/null +++ b/site/src/playground/theme-lab-assets/penguins-violin.nature.json @@ -0,0 +1,195 @@ +{ + "$schema": "https://vega-lite.github.io/schema/vega-lite/v5.json", + "__theme__": "nature", + "__design__": [ + "Every observation drawn on top of its own density curve. A kernel density estimate from eleven penguins is a smooth curve asserting a shape that eleven points cannot establish; a figure panel has to let the reviewer see how much sample the curve was built from.", + "n stated per panel in the facet header. Flint labels each panel with the species name alone, which is the one thing the reader could already infer from the axis.", + "Density extent rounded to [2700, 6500] g. The baseline inherits an extent of [2702.9783569331094, 6447.021643066891] — a number no methods section could reproduce and no reader could justify.", + "Violins given a pale fill with a 0.8px black outline instead of a solid hue. Shape has to survive greyscale reproduction, so the outline carries the form and the colour is redundant reinforcement rather than the only cue.", + "Okabe–Ito palette replacing Flint's category default, and the same three hues used for both the fill and the points so a species reads as one object across the two marks.", + "Median marked with a short black rule inside each violin — the single summary statistic a distribution figure is normally asked for, and the one thing a density curve makes hardest to locate.", + "Black L-shaped spines with outward ticks, no gridlines, 8.5pt type, units mandatory in the axis title, and the density axis left unlabelled because its units are arbitrary and labelling them would invite a comparison that is not meaningful." + ], + "background": "#ffffff", + "padding": { "left": 4, "top": 4, "right": 6, "bottom": 4 }, + "title": { + "text": "Body mass by penguin species", + "subtitle": ["Kernel density with all observations overlaid; horizontal rule marks the median"] + }, + "data": { + "values": [ + { "Species": "Adelie", "Body mass (g)": 3750 }, + { "Species": "Adelie", "Body mass (g)": 3800 }, + { "Species": "Adelie", "Body mass (g)": 3250 }, + { "Species": "Adelie", "Body mass (g)": 3450 }, + { "Species": "Adelie", "Body mass (g)": 3650 }, + { "Species": "Adelie", "Body mass (g)": 3625 }, + { "Species": "Adelie", "Body mass (g)": 4675 }, + { "Species": "Adelie", "Body mass (g)": 3200 }, + { "Species": "Adelie", "Body mass (g)": 3800 }, + { "Species": "Adelie", "Body mass (g)": 4400 }, + { "Species": "Adelie", "Body mass (g)": 3700 }, + { "Species": "Chinstrap", "Body mass (g)": 3500 }, + { "Species": "Chinstrap", "Body mass (g)": 3900 }, + { "Species": "Chinstrap", "Body mass (g)": 3650 }, + { "Species": "Chinstrap", "Body mass (g)": 3525 }, + { "Species": "Chinstrap", "Body mass (g)": 3950 }, + { "Species": "Chinstrap", "Body mass (g)": 3800 }, + { "Species": "Chinstrap", "Body mass (g)": 3300 }, + { "Species": "Chinstrap", "Body mass (g)": 4800 }, + { "Species": "Chinstrap", "Body mass (g)": 4050 }, + { "Species": "Chinstrap", "Body mass (g)": 3550 }, + { "Species": "Gentoo", "Body mass (g)": 4500 }, + { "Species": "Gentoo", "Body mass (g)": 5700 }, + { "Species": "Gentoo", "Body mass (g)": 4450 }, + { "Species": "Gentoo", "Body mass (g)": 5700 }, + { "Species": "Gentoo", "Body mass (g)": 5400 }, + { "Species": "Gentoo", "Body mass (g)": 5550 }, + { "Species": "Gentoo", "Body mass (g)": 4800 }, + { "Species": "Gentoo", "Body mass (g)": 5000 }, + { "Species": "Gentoo", "Body mass (g)": 4650 }, + { "Species": "Gentoo", "Body mass (g)": 5550 }, + { "Species": "Gentoo", "Body mass (g)": 5950 }, + { "Species": "Gentoo", "Body mass (g)": 5250 } + ] + }, + "transform": [ + { "joinaggregate": [{ "op": "count", "as": "__n" }], "groupby": ["Species"] }, + { "calculate": "datum.Species + ' (n = ' + datum.__n + ')'", "as": "__panel" } + ], + "facet": { + "field": "__panel", + "type": "nominal", + "sort": ["Adelie (n = 11)", "Chinstrap (n = 10)", "Gentoo (n = 12)"], + "title": null, + "header": { + "labelOrient": "bottom", + "labelPadding": 4, + "labelFontSize": 8.5, + "labelColor": "#000000" + } + }, + "columns": 3, + "spacing": 4, + "resolve": { "scale": { "x": "shared" } }, + "spec": { + "width": 68, + "height": 175, + "encoding": { + "y": { + "field": "Body mass (g)", + "type": "quantitative", + "title": "Body mass (g)", + "scale": { "zero": false, "nice": false, "domain": [2700, 6500] }, + "axis": { "tickCount": 5, "format": ",d" } + }, + "color": { + "field": "Species", + "type": "nominal", + "legend": null, + "scale": { + "domain": ["Adelie", "Chinstrap", "Gentoo"], + "range": ["#0072b2", "#e69f00", "#009e73"] + } + } + }, + "layer": [ + { + "transform": [ + { + "density": "Body mass (g)", + "groupby": ["Species"], + "extent": [2700, 6500], + "as": ["__value", "__density"] + }, + { "calculate": "-datum.__density / 2", "as": "__xLeft" }, + { "calculate": "datum.__density / 2", "as": "__xRight" } + ], + "mark": { + "type": "area", + "orient": "horizontal", + "opacity": 0.28, + "stroke": "#000000", + "strokeWidth": 0.8, + "strokeOpacity": 1 + }, + "encoding": { + "y": { "field": "__value", "type": "quantitative" }, + "x": { + "field": "__xLeft", + "type": "quantitative", + "title": null, + "scale": { "nice": false, "domain": [-0.0006, 0.0006] }, + "axis": null + }, + "x2": { "field": "__xRight" } + } + }, + { + "transform": [{ "calculate": "(random() * 2 - 1) * 0.00019", "as": "__jx" }], + "mark": { + "type": "point", + "filled": true, + "size": 14, + "opacity": 0.85, + "stroke": "#ffffff", + "strokeWidth": 0.4 + }, + "encoding": { + "x": { "field": "__jx", "type": "quantitative", "axis": null } + } + }, + { + "transform": [ + { "aggregate": [{ "op": "median", "field": "Body mass (g)", "as": "__median" }], "groupby": ["Species"] } + ], + "mark": { "type": "rule", "color": "#000000", "strokeWidth": 1.2 }, + "encoding": { + "y": { "field": "__median", "type": "quantitative" }, + "x": { "datum": -0.00028, "type": "quantitative", "axis": null }, + "x2": { "datum": 0.00028 } + } + } + ] + }, + "config": { + "background": "#ffffff", + "font": "Arial, Helvetica, sans-serif", + "title": { + "font": "Arial, Helvetica, sans-serif", + "fontSize": 12, + "fontWeight": 700, + "color": "#000000", + "anchor": "start", + "offset": 6, + "subtitleFont": "Arial, Helvetica, sans-serif", + "subtitleFontSize": 8, + "subtitleFontStyle": "italic", + "subtitleColor": "#3c3c3c", + "subtitlePadding": 5 + }, + "view": { "stroke": null }, + "axis": { + "labelFont": "Arial, Helvetica, sans-serif", + "labelFontSize": 8.5, + "labelColor": "#000000", + "labelPadding": 2, + "titleFont": "Arial, Helvetica, sans-serif", + "titleFontSize": 9, + "titleFontWeight": 400, + "titleColor": "#000000", + "titlePadding": 4, + "grid": false, + "domain": true, + "domainColor": "#000000", + "domainWidth": 1, + "ticks": true, + "tickColor": "#000000", + "tickWidth": 1, + "tickSize": 3.5 + }, + "header": { + "labelFont": "Arial, Helvetica, sans-serif" + } + } +} diff --git a/site/src/playground/theme-lab-assets/penguins.flint.json b/site/src/playground/theme-lab-assets/penguins.flint.json new file mode 100644 index 00000000..e74b5df2 --- /dev/null +++ b/site/src/playground/theme-lab-assets/penguins.flint.json @@ -0,0 +1,230 @@ +{ + "mark": "circle", + "encoding": { + "x": { + "field": "Flipper length (mm)", + "type": "quantitative", + "scale": { + "zero": false + }, + "axis": { + "format": ",.12~g" + } + }, + "y": { + "field": "Body mass (g)", + "type": "quantitative", + "scale": { + "zero": false + }, + "axis": { + "format": ",.12~g" + } + }, + "color": { + "field": "Species", + "type": "nominal", + "sort": null, + "scale": { + "scheme": "tableau10" + } + } + }, + "config": { + "view": { + "continuousWidth": 306, + "continuousHeight": 256 + }, + "axisX": { + "labelLimit": 100, + "labelFontSize": 10, + "titleFontSize": 11 + }, + "axisY": { + "labelFontSize": 10, + "titleFontSize": 11 + }, + "legend": { + "labelFontSize": 10, + "titleFontSize": 11 + }, + "facet": { + "spacing": 26 + } + }, + "data": { + "values": [ + { + "Species": "Adelie", + "Flipper length (mm)": 181, + "Body mass (g)": 3750 + }, + { + "Species": "Adelie", + "Flipper length (mm)": 186, + "Body mass (g)": 3800 + }, + { + "Species": "Adelie", + "Flipper length (mm)": 195, + "Body mass (g)": 3250 + }, + { + "Species": "Adelie", + "Flipper length (mm)": 193, + "Body mass (g)": 3450 + }, + { + "Species": "Adelie", + "Flipper length (mm)": 190, + "Body mass (g)": 3650 + }, + { + "Species": "Adelie", + "Flipper length (mm)": 181, + "Body mass (g)": 3625 + }, + { + "Species": "Adelie", + "Flipper length (mm)": 195, + "Body mass (g)": 4675 + }, + { + "Species": "Adelie", + "Flipper length (mm)": 182, + "Body mass (g)": 3200 + }, + { + "Species": "Adelie", + "Flipper length (mm)": 191, + "Body mass (g)": 3800 + }, + { + "Species": "Adelie", + "Flipper length (mm)": 198, + "Body mass (g)": 4400 + }, + { + "Species": "Adelie", + "Flipper length (mm)": 185, + "Body mass (g)": 3700 + }, + { + "Species": "Chinstrap", + "Flipper length (mm)": 192, + "Body mass (g)": 3500 + }, + { + "Species": "Chinstrap", + "Flipper length (mm)": 196, + "Body mass (g)": 3900 + }, + { + "Species": "Chinstrap", + "Flipper length (mm)": 193, + "Body mass (g)": 3650 + }, + { + "Species": "Chinstrap", + "Flipper length (mm)": 188, + "Body mass (g)": 3525 + }, + { + "Species": "Chinstrap", + "Flipper length (mm)": 197, + "Body mass (g)": 3950 + }, + { + "Species": "Chinstrap", + "Flipper length (mm)": 198, + "Body mass (g)": 3800 + }, + { + "Species": "Chinstrap", + "Flipper length (mm)": 178, + "Body mass (g)": 3300 + }, + { + "Species": "Chinstrap", + "Flipper length (mm)": 207, + "Body mass (g)": 4800 + }, + { + "Species": "Chinstrap", + "Flipper length (mm)": 201, + "Body mass (g)": 4050 + }, + { + "Species": "Chinstrap", + "Flipper length (mm)": 191, + "Body mass (g)": 3550 + }, + { + "Species": "Gentoo", + "Flipper length (mm)": 211, + "Body mass (g)": 4500 + }, + { + "Species": "Gentoo", + "Flipper length (mm)": 230, + "Body mass (g)": 5700 + }, + { + "Species": "Gentoo", + "Flipper length (mm)": 210, + "Body mass (g)": 4450 + }, + { + "Species": "Gentoo", + "Flipper length (mm)": 218, + "Body mass (g)": 5700 + }, + { + "Species": "Gentoo", + "Flipper length (mm)": 215, + "Body mass (g)": 5400 + }, + { + "Species": "Gentoo", + "Flipper length (mm)": 219, + "Body mass (g)": 5550 + }, + { + "Species": "Gentoo", + "Flipper length (mm)": 209, + "Body mass (g)": 4800 + }, + { + "Species": "Gentoo", + "Flipper length (mm)": 215, + "Body mass (g)": 5000 + }, + { + "Species": "Gentoo", + "Flipper length (mm)": 214, + "Body mass (g)": 4650 + }, + { + "Species": "Gentoo", + "Flipper length (mm)": 216, + "Body mass (g)": 5550 + }, + { + "Species": "Gentoo", + "Flipper length (mm)": 221, + "Body mass (g)": 5950 + }, + { + "Species": "Gentoo", + "Flipper length (mm)": 217, + "Body mass (g)": 5250 + } + ] + }, + "title": { + "text": "Flipper length versus body mass in three penguin species", + "subtitle": [ + "Palmer Archipelago, Antarctica; n = 33" + ] + } +} diff --git a/site/src/playground/theme-lab-assets/penguins.nature.json b/site/src/playground/theme-lab-assets/penguins.nature.json new file mode 100644 index 00000000..397d5107 --- /dev/null +++ b/site/src/playground/theme-lab-assets/penguins.nature.json @@ -0,0 +1,145 @@ +{ + "$schema": "https://vega-lite.github.io/schema/vega-lite/v5.json", + "__theme__": "nature", + "__design__": [ + "Grid deleted entirely and the frame replaced by black L-shaped spines with outward 3.5pt ticks. Flint's default kept a light box and no domain lines; a figure panel needs the opposite.", + "Palette switched from tableau10 to Okabe–Ito (#0072B2 / #E69F00 / #009E73), which stays separable in deuteranopia and in greyscale.", + "Redundant encoding added: species also maps to shape, and the two legends merge into one, so colour is never the only channel carrying the group.", + "Units made mandatory in both axis titles; type dropped to 8.5pt labels / 9pt titles for an 89 mm single-column figure.", + "Legend kept but moved inside the top-left of the plot — a journal figure pays for width, not for a legend gutter.", + "Point size cut to 26 with a thin white stroke so overlapping birds stay countable at print size, and mark type changed from circle to point so the shape channel is available at all.", + "Headline set as a small 12pt bold sans line with the sample description demoted to an 8pt italic deck — the journal convention of a terse label over a methods line, rather than one undifferentiated title string." + ], + "background": "#ffffff", + "padding": { "left": 4, "top": 4, "right": 6, "bottom": 4 }, + "width": 250, + "height": 195, + "title": { + "text": "Flipper length versus body mass in three penguin species", + "subtitle": ["Palmer Archipelago, Antarctica; n = 33"] + }, + "data": { + "values": [ + { "Species": "Adelie", "Flipper length (mm)": 181, "Body mass (g)": 3750 }, + { "Species": "Adelie", "Flipper length (mm)": 186, "Body mass (g)": 3800 }, + { "Species": "Adelie", "Flipper length (mm)": 195, "Body mass (g)": 3250 }, + { "Species": "Adelie", "Flipper length (mm)": 193, "Body mass (g)": 3450 }, + { "Species": "Adelie", "Flipper length (mm)": 190, "Body mass (g)": 3650 }, + { "Species": "Adelie", "Flipper length (mm)": 181, "Body mass (g)": 3625 }, + { "Species": "Adelie", "Flipper length (mm)": 195, "Body mass (g)": 4675 }, + { "Species": "Adelie", "Flipper length (mm)": 182, "Body mass (g)": 3200 }, + { "Species": "Adelie", "Flipper length (mm)": 191, "Body mass (g)": 3800 }, + { "Species": "Adelie", "Flipper length (mm)": 198, "Body mass (g)": 4400 }, + { "Species": "Adelie", "Flipper length (mm)": 185, "Body mass (g)": 3700 }, + { "Species": "Chinstrap", "Flipper length (mm)": 192, "Body mass (g)": 3500 }, + { "Species": "Chinstrap", "Flipper length (mm)": 196, "Body mass (g)": 3900 }, + { "Species": "Chinstrap", "Flipper length (mm)": 193, "Body mass (g)": 3650 }, + { "Species": "Chinstrap", "Flipper length (mm)": 188, "Body mass (g)": 3525 }, + { "Species": "Chinstrap", "Flipper length (mm)": 197, "Body mass (g)": 3950 }, + { "Species": "Chinstrap", "Flipper length (mm)": 198, "Body mass (g)": 3800 }, + { "Species": "Chinstrap", "Flipper length (mm)": 178, "Body mass (g)": 3300 }, + { "Species": "Chinstrap", "Flipper length (mm)": 207, "Body mass (g)": 4800 }, + { "Species": "Chinstrap", "Flipper length (mm)": 201, "Body mass (g)": 4050 }, + { "Species": "Chinstrap", "Flipper length (mm)": 191, "Body mass (g)": 3550 }, + { "Species": "Gentoo", "Flipper length (mm)": 211, "Body mass (g)": 4500 }, + { "Species": "Gentoo", "Flipper length (mm)": 230, "Body mass (g)": 5700 }, + { "Species": "Gentoo", "Flipper length (mm)": 210, "Body mass (g)": 4450 }, + { "Species": "Gentoo", "Flipper length (mm)": 218, "Body mass (g)": 5700 }, + { "Species": "Gentoo", "Flipper length (mm)": 215, "Body mass (g)": 5400 }, + { "Species": "Gentoo", "Flipper length (mm)": 219, "Body mass (g)": 5550 }, + { "Species": "Gentoo", "Flipper length (mm)": 209, "Body mass (g)": 4800 }, + { "Species": "Gentoo", "Flipper length (mm)": 215, "Body mass (g)": 5000 }, + { "Species": "Gentoo", "Flipper length (mm)": 214, "Body mass (g)": 4650 }, + { "Species": "Gentoo", "Flipper length (mm)": 216, "Body mass (g)": 5550 }, + { "Species": "Gentoo", "Flipper length (mm)": 221, "Body mass (g)": 5950 }, + { "Species": "Gentoo", "Flipper length (mm)": 217, "Body mass (g)": 5250 } + ] + }, + "mark": { "type": "point", "filled": true, "size": 26, "opacity": 0.95, "stroke": "#ffffff", "strokeWidth": 0.4 }, + "encoding": { + "x": { + "field": "Flipper length (mm)", + "type": "quantitative", + "title": "Flipper length (mm)", + "scale": { "zero": false, "nice": true }, + "axis": { "tickCount": 5 } + }, + "y": { + "field": "Body mass (g)", + "type": "quantitative", + "title": "Body mass (g)", + "scale": { "zero": false, "nice": true }, + "axis": { "tickCount": 5, "format": ",.0f" } + }, + "color": { + "field": "Species", + "type": "nominal", + "sort": null, + "title": "Species", + "scale": { "range": ["#0072b2", "#e69f00", "#009e73"] }, + "legend": { + "orient": "top-left", + "direction": "vertical", + "fillColor": "#ffffff", + "strokeColor": null, + "offset": 4, + "padding": 2 + } + }, + "shape": { + "field": "Species", + "type": "nominal", + "sort": null, + "title": "Species", + "scale": { "range": ["circle", "triangle-up", "square"] } + } + }, + "config": { + "background": "#ffffff", + "font": "Arial, Helvetica, sans-serif", + "title": { + "font": "Arial, Helvetica, sans-serif", + "fontSize": 12, + "fontWeight": 700, + "color": "#000000", + "anchor": "start", + "offset": 6, + "subtitleFont": "Arial, Helvetica, sans-serif", + "subtitleFontSize": 8, + "subtitleFontStyle": "italic", + "subtitleColor": "#3c3c3c", + "subtitlePadding": 5 + }, + "view": { "stroke": null }, + "axis": { + "labelFont": "Arial, Helvetica, sans-serif", + "labelFontSize": 8.5, + "labelColor": "#000000", + "labelPadding": 2, + "titleFont": "Arial, Helvetica, sans-serif", + "titleFontSize": 9, + "titleFontWeight": 400, + "titleColor": "#000000", + "titlePadding": 4, + "grid": false, + "domain": true, + "domainColor": "#000000", + "domainWidth": 1, + "ticks": true, + "tickColor": "#000000", + "tickWidth": 1, + "tickSize": 3.5 + }, + "legend": { + "labelFont": "Arial, Helvetica, sans-serif", + "labelFontSize": 8.5, + "labelColor": "#000000", + "titleFont": "Arial, Helvetica, sans-serif", + "titleFontSize": 9, + "titleFontWeight": 400, + "titleColor": "#000000", + "symbolSize": 30, + "rowPadding": 0 + } + } +} diff --git a/site/src/playground/theme-lab-assets/population-region.datawrapper.json b/site/src/playground/theme-lab-assets/population-region.datawrapper.json new file mode 100644 index 00000000..a628f0a5 --- /dev/null +++ b/site/src/playground/theme-lab-assets/population-region.datawrapper.json @@ -0,0 +1,153 @@ +{ + "$schema": "https://vega-lite.github.io/schema/vega-lite/v5.json", + "__theme__": "datawrapper", + "__design__": [ + "Type floor raised: 11pt axis labels and 11pt legend against Flint's 10pt. A civic chart has to survive being downscaled onto a phone.", + "Legend moved to the top with 100-unit square symbols and no title — it reads as a colour key strip rather than a boxed sidebar on the right.", + "Stack order pinned to the sequence the baseline already produces (Asia → Africa → Europe → Americas → Oceania) rather than left to 'sort: null', so the spec states the sequence instead of inheriting it from row order. The sequence itself is unchanged.", + "Scheme replaced by an explicit five-colour domain/range pair, so a region keeps the same colour across every chart in a story.", + "Gridlines switched to a light dashed rule (#DCDCDC, [2,2]) that reads behind the areas; the chart frame and y-axis domain are removed, but the x baseline is kept in near-black for contrast.", + "Y-axis title rotated flat to a bare unit ('millions'); x-axis title deleted and years formatted %Y.", + "A hairline footer rule is appended as a second vconcat row — it closes the chart block visually and is the anchor a source line would attach to." + ], + "background": "#ffffff", + "padding": { "left": 8, "top": 6, "right": 14, "bottom": 8 }, + "title": { + "text": "World population by region", + "subtitle": ["1950–2020, millions of people"] + }, + "spacing": 8, + "vconcat": [ + { + "width": 280, + "height": 190, + "data": { + "values": [ + { "Year": "1950", "Region": "Asia", "Population": 1404 }, + { "Year": "1950", "Region": "Africa", "Population": 227 }, + { "Year": "1950", "Region": "Europe", "Population": 549 }, + { "Year": "1950", "Region": "Americas", "Population": 339 }, + { "Year": "1950", "Region": "Oceania", "Population": 13 }, + { "Year": "1970", "Region": "Asia", "Population": 2142 }, + { "Year": "1970", "Region": "Africa", "Population": 365 }, + { "Year": "1970", "Region": "Europe", "Population": 657 }, + { "Year": "1970", "Region": "Americas", "Population": 512 }, + { "Year": "1970", "Region": "Oceania", "Population": 20 }, + { "Year": "1990", "Region": "Asia", "Population": 3226 }, + { "Year": "1990", "Region": "Africa", "Population": 630 }, + { "Year": "1990", "Region": "Europe", "Population": 721 }, + { "Year": "1990", "Region": "Americas", "Population": 724 }, + { "Year": "1990", "Region": "Oceania", "Population": 27 }, + { "Year": "2010", "Region": "Asia", "Population": 4194 }, + { "Year": "2010", "Region": "Africa", "Population": 1039 }, + { "Year": "2010", "Region": "Europe", "Population": 736 }, + { "Year": "2010", "Region": "Americas", "Population": 934 }, + { "Year": "2010", "Region": "Oceania", "Population": 37 }, + { "Year": "2020", "Region": "Asia", "Population": 4641 }, + { "Year": "2020", "Region": "Africa", "Population": 1361 }, + { "Year": "2020", "Region": "Europe", "Population": 748 }, + { "Year": "2020", "Region": "Americas", "Population": 1023 }, + { "Year": "2020", "Region": "Oceania", "Population": 45 } + ] + }, + "mark": { "type": "area", "line": false, "opacity": 1 }, + "encoding": { + "x": { + "field": "Year", + "type": "temporal", + "title": null, + "axis": { "format": "%Y", "tickCount": 5, "domain": true, "domainColor": "#333333", "grid": false } + }, + "y": { + "field": "Population", + "type": "quantitative", + "stack": "zero", + "title": "millions", + "axis": { + "format": ",.0f", + "tickCount": 4, + "grid": true, + "gridColor": "#dcdcdc", + "gridDash": [2, 2], + "domain": false, + "ticks": false, + "titleAngle": 0, + "titleAlign": "left", + "titleAnchor": "start", + "titleBaseline": "bottom", + "titleY": -8, + "titlePadding": 0 + } + }, + "color": { + "field": "Region", + "type": "nominal", + "title": null, + "sort": ["Asia", "Africa", "Europe", "Americas", "Oceania"], + "scale": { + "domain": ["Asia", "Africa", "Europe", "Americas", "Oceania"], + "range": ["#18a1cd", "#e2a233", "#2d8659", "#c04a4a", "#7e5aa2"] + }, + "legend": { + "orient": "top", + "direction": "horizontal", + "symbolType": "square", + "symbolSize": 100, + "columnPadding": 12, + "offset": 4, + "padding": 0 + } + }, + "order": { + "field": "Region", + "type": "nominal", + "sort": ["Asia", "Africa", "Europe", "Americas", "Oceania"] + } + } + }, + { + "data": { "values": [{}] }, + "mark": { "type": "rect", "fill": "#dcdcdc", "stroke": null }, + "width": 300, + "height": 1, + "view": { "stroke": null } + } + ], + "config": { + "background": "#ffffff", + "font": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "title": { + "font": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "fontSize": 14.5, + "fontWeight": 700, + "color": "#333333", + "anchor": "start", + "offset": 12, + "lineHeight": 18, + "subtitleFont": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "subtitleFontSize": 11.5, + "subtitleColor": "#666666", + "subtitlePadding": 7, + "subtitleLineHeight": 15 + }, + "view": { "stroke": null }, + "axis": { + "labelFont": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "labelFontSize": 11, + "labelColor": "#333333", + "labelPadding": 5, + "titleFont": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "titleFontSize": 10.5, + "titleFontWeight": 400, + "titleColor": "#767676", + "grid": false, + "domain": false, + "ticks": false + }, + "legend": { + "labelFont": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "labelFontSize": 11, + "labelColor": "#333333" + } + } +} diff --git a/site/src/playground/theme-lab-assets/population-region.flint.json b/site/src/playground/theme-lab-assets/population-region.flint.json new file mode 100644 index 00000000..61fbd268 --- /dev/null +++ b/site/src/playground/theme-lab-assets/population-region.flint.json @@ -0,0 +1,191 @@ +{ + "mark": "area", + "encoding": { + "x": { + "field": "Year", + "type": "temporal" + }, + "y": { + "field": "Population", + "type": "quantitative", + "scale": { + "zero": true + }, + "axis": { + "format": ",.12~g" + } + }, + "color": { + "field": "Region", + "type": "nominal", + "sort": null, + "scale": { + "scheme": "tableau10", + "domain": [ + "Asia", + "Africa", + "Europe", + "Americas", + "Oceania" + ] + } + } + }, + "config": { + "view": { + "continuousWidth": 308, + "continuousHeight": 254 + }, + "axisX": { + "labelLimit": 100, + "labelFontSize": 10, + "titleFontSize": 11 + }, + "axisY": { + "labelFontSize": 10, + "titleFontSize": 11 + }, + "legend": { + "labelFontSize": 10, + "titleFontSize": 11 + }, + "facet": { + "spacing": 25 + } + }, + "data": { + "values": [ + { + "Year": "1950", + "Region": "Asia", + "Population": 1404 + }, + { + "Year": "1950", + "Region": "Africa", + "Population": 227 + }, + { + "Year": "1950", + "Region": "Europe", + "Population": 549 + }, + { + "Year": "1950", + "Region": "Americas", + "Population": 339 + }, + { + "Year": "1950", + "Region": "Oceania", + "Population": 13 + }, + { + "Year": "1970", + "Region": "Asia", + "Population": 2142 + }, + { + "Year": "1970", + "Region": "Africa", + "Population": 365 + }, + { + "Year": "1970", + "Region": "Europe", + "Population": 657 + }, + { + "Year": "1970", + "Region": "Americas", + "Population": 512 + }, + { + "Year": "1970", + "Region": "Oceania", + "Population": 20 + }, + { + "Year": "1990", + "Region": "Asia", + "Population": 3226 + }, + { + "Year": "1990", + "Region": "Africa", + "Population": 630 + }, + { + "Year": "1990", + "Region": "Europe", + "Population": 721 + }, + { + "Year": "1990", + "Region": "Americas", + "Population": 724 + }, + { + "Year": "1990", + "Region": "Oceania", + "Population": 27 + }, + { + "Year": "2010", + "Region": "Asia", + "Population": 4194 + }, + { + "Year": "2010", + "Region": "Africa", + "Population": 1039 + }, + { + "Year": "2010", + "Region": "Europe", + "Population": 736 + }, + { + "Year": "2010", + "Region": "Americas", + "Population": 934 + }, + { + "Year": "2010", + "Region": "Oceania", + "Population": 37 + }, + { + "Year": "2020", + "Region": "Asia", + "Population": 4641 + }, + { + "Year": "2020", + "Region": "Africa", + "Population": 1361 + }, + { + "Year": "2020", + "Region": "Europe", + "Population": 748 + }, + { + "Year": "2020", + "Region": "Americas", + "Population": 1023 + }, + { + "Year": "2020", + "Region": "Oceania", + "Population": 45 + } + ] + }, + "title": { + "text": "World population by region", + "subtitle": [ + "1950–2020, millions of people" + ] + } +} diff --git a/site/src/playground/theme-lab-assets/population-stream.flint.json b/site/src/playground/theme-lab-assets/population-stream.flint.json new file mode 100644 index 00000000..abf6a906 --- /dev/null +++ b/site/src/playground/theme-lab-assets/population-stream.flint.json @@ -0,0 +1,190 @@ +{ + "mark": "area", + "encoding": { + "x": { + "field": "Year", + "type": "temporal" + }, + "y": { + "field": "Population", + "type": "quantitative", + "stack": "center", + "axis": null, + "scale": { + "zero": true + } + }, + "color": { + "field": "Region", + "type": "nominal", + "sort": null, + "scale": { + "scheme": "tableau10", + "domain": [ + "Asia", + "Africa", + "Europe", + "Americas", + "Oceania" + ] + } + } + }, + "config": { + "view": { + "continuousWidth": 308, + "continuousHeight": 254 + }, + "axisX": { + "labelLimit": 100, + "labelFontSize": 10, + "titleFontSize": 11 + }, + "axisY": { + "labelFontSize": 10, + "titleFontSize": 11 + }, + "legend": { + "labelFontSize": 10, + "titleFontSize": 11 + }, + "facet": { + "spacing": 25 + } + }, + "data": { + "values": [ + { + "Year": "1950", + "Region": "Asia", + "Population": 1404 + }, + { + "Year": "1950", + "Region": "Africa", + "Population": 227 + }, + { + "Year": "1950", + "Region": "Europe", + "Population": 549 + }, + { + "Year": "1950", + "Region": "Americas", + "Population": 339 + }, + { + "Year": "1950", + "Region": "Oceania", + "Population": 13 + }, + { + "Year": "1970", + "Region": "Asia", + "Population": 2142 + }, + { + "Year": "1970", + "Region": "Africa", + "Population": 365 + }, + { + "Year": "1970", + "Region": "Europe", + "Population": 657 + }, + { + "Year": "1970", + "Region": "Americas", + "Population": 512 + }, + { + "Year": "1970", + "Region": "Oceania", + "Population": 20 + }, + { + "Year": "1990", + "Region": "Asia", + "Population": 3226 + }, + { + "Year": "1990", + "Region": "Africa", + "Population": 630 + }, + { + "Year": "1990", + "Region": "Europe", + "Population": 721 + }, + { + "Year": "1990", + "Region": "Americas", + "Population": 724 + }, + { + "Year": "1990", + "Region": "Oceania", + "Population": 27 + }, + { + "Year": "2010", + "Region": "Asia", + "Population": 4194 + }, + { + "Year": "2010", + "Region": "Africa", + "Population": 1039 + }, + { + "Year": "2010", + "Region": "Europe", + "Population": 736 + }, + { + "Year": "2010", + "Region": "Americas", + "Population": 934 + }, + { + "Year": "2010", + "Region": "Oceania", + "Population": 37 + }, + { + "Year": "2020", + "Region": "Asia", + "Population": 4641 + }, + { + "Year": "2020", + "Region": "Africa", + "Population": 1361 + }, + { + "Year": "2020", + "Region": "Europe", + "Population": 748 + }, + { + "Year": "2020", + "Region": "Americas", + "Population": 1023 + }, + { + "Year": "2020", + "Region": "Oceania", + "Population": 45 + } + ] + }, + "title": { + "text": "Where the world's people are", + "subtitle": [ + "Population by region, 1950–2020, millions" + ] + } +} diff --git a/site/src/playground/theme-lab-assets/population-stream.nyt.json b/site/src/playground/theme-lab-assets/population-stream.nyt.json new file mode 100644 index 00000000..3c8bfcc0 --- /dev/null +++ b/site/src/playground/theme-lab-assets/population-stream.nyt.json @@ -0,0 +1,141 @@ +{ + "$schema": "https://vega-lite.github.io/schema/vega-lite/v5.json", + "__theme__": "nyt", + "__design__": [ + "Legend deleted and each region named inside its own ribbon, with its 2020 figure attached. A streamgraph has no value axis to read against — Flint correctly sets axis null — so a right-hand key leaves the reader with shapes they cannot convert into numbers.", + "Stacking taken off the encoding and onto an explicit stack transform with offset 'center', so the label layer can compute each ribbon's midpoint from the same y0/y1 the area is drawn from. Positioning labels by eye on a centred stack is how they end up half a band out. The transform sorts descending because Vega-Lite draws the first colour-domain entry at the TOP of a vertical stack, and the two columns have to read in the same direction.", + "Oceania's label dropped just below its ribbon rather than sitting inside it: at 45m against a 7.8bn total the band is about one pixel tall, and a design that only labels the bands that happen to be thick enough is a design that hides its smallest category.", + "Gridlines turned vertical, not horizontal. On a centred stack a horizontal rule means nothing — there is no fixed baseline for it to measure from — whereas a vertical rule at each observed year marks where the thicknesses are actually comparable.", + "Five NYT swatches replacing tableau10, with labels reversed out in white; the ribbons carry saturated ink because on a stream the fill *is* the mark, not a container for one.", + "Serif headline flush left, frame and axis domain removed, x reduced to the five years where data exists — the baseline's temporal axis was interpolating tick positions between measurements." + ], + "background": "#ffffff", + "padding": { "left": 8, "top": 6, "right": 10, "bottom": 6 }, + "width": 300, + "height": 200, + "title": { + "text": "Where the world's people are", + "subtitle": ["Population by region, 1950–2020, millions"] + }, + "data": { + "values": [ + { "Year": "1950", "Region": "Asia", "Population": 1404 }, + { "Year": "1950", "Region": "Africa", "Population": 227 }, + { "Year": "1950", "Region": "Europe", "Population": 549 }, + { "Year": "1950", "Region": "Americas", "Population": 339 }, + { "Year": "1950", "Region": "Oceania", "Population": 13 }, + { "Year": "1970", "Region": "Asia", "Population": 2142 }, + { "Year": "1970", "Region": "Africa", "Population": 365 }, + { "Year": "1970", "Region": "Europe", "Population": 657 }, + { "Year": "1970", "Region": "Americas", "Population": 512 }, + { "Year": "1970", "Region": "Oceania", "Population": 20 }, + { "Year": "1990", "Region": "Asia", "Population": 3226 }, + { "Year": "1990", "Region": "Africa", "Population": 630 }, + { "Year": "1990", "Region": "Europe", "Population": 721 }, + { "Year": "1990", "Region": "Americas", "Population": 724 }, + { "Year": "1990", "Region": "Oceania", "Population": 27 }, + { "Year": "2010", "Region": "Asia", "Population": 4194 }, + { "Year": "2010", "Region": "Africa", "Population": 1039 }, + { "Year": "2010", "Region": "Europe", "Population": 736 }, + { "Year": "2010", "Region": "Americas", "Population": 934 }, + { "Year": "2010", "Region": "Oceania", "Population": 37 }, + { "Year": "2020", "Region": "Asia", "Population": 4641 }, + { "Year": "2020", "Region": "Africa", "Population": 1361 }, + { "Year": "2020", "Region": "Europe", "Population": 748 }, + { "Year": "2020", "Region": "Americas", "Population": 1023 }, + { "Year": "2020", "Region": "Oceania", "Population": 45 } + ] + }, + "transform": [ + { + "calculate": "indexof(['Asia', 'Africa', 'Europe', 'Americas', 'Oceania'], datum.Region)", + "as": "__band" + }, + { + "stack": "Population", + "groupby": ["Year"], + "sort": [{ "field": "__band", "order": "descending" }], + "offset": "center", + "as": ["__y0", "__y1"] + }, + { + "calculate": "datum.Region === 'Oceania' ? datum.__y0 - 270 : (datum.__y0 + datum.__y1) / 2", + "as": "__labelY" + }, + { + "calculate": "datum.Region + ' ' + format(datum.Population, ',')", + "as": "__label" + } + ], + "encoding": { + "x": { + "field": "Year", + "type": "ordinal", + "title": null, + "scale": { "padding": 0 }, + "axis": { "labelAngle": 0, "labelPadding": 6, "grid": true, "gridColor": "#e4e4e4", "gridDash": [2, 2] } + }, + "color": { + "field": "Region", + "type": "nominal", + "legend": null, + "scale": { + "domain": ["Asia", "Africa", "Europe", "Americas", "Oceania"], + "range": ["#c2352b", "#d9a441", "#4a8b6f", "#2f6b9a", "#7f6a9e"] + } + } + }, + "layer": [ + { + "mark": { "type": "area", "interpolate": "monotone", "line": false }, + "encoding": { + "y": { "field": "__y0", "type": "quantitative", "title": null, "axis": null }, + "y2": { "field": "__y1" } + } + }, + { + "transform": [{ "filter": "datum.Year === '2020'" }], + "mark": { + "type": "text", + "align": "right", + "baseline": "middle", + "dx": -7, + "fontSize": 9.5, + "fontWeight": 600 + }, + "encoding": { + "y": { "field": "__labelY", "type": "quantitative", "axis": null }, + "text": { "field": "__label", "type": "nominal" }, + "color": { + "condition": { "test": "datum.Region === 'Oceania'", "value": "#121212" }, + "value": "#ffffff" + } + } + } + ], + "config": { + "background": "#ffffff", + "font": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "title": { + "font": "Georgia, 'Times New Roman', Times, serif", + "fontSize": 15, + "fontWeight": 700, + "color": "#121212", + "anchor": "start", + "offset": 14, + "subtitleFont": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "subtitleFontSize": 10.5, + "subtitleColor": "#6b6b6b", + "subtitlePadding": 7 + }, + "view": { "stroke": null }, + "axis": { + "labelFont": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "labelFontSize": 10, + "labelColor": "#6b6b6b", + "domain": false, + "ticks": false, + "grid": false + } + } +} diff --git a/site/src/playground/theme-lab-assets/population-waterfall.flint.json b/site/src/playground/theme-lab-assets/population-waterfall.flint.json new file mode 100644 index 00000000..79d3bf03 --- /dev/null +++ b/site/src/playground/theme-lab-assets/population-waterfall.flint.json @@ -0,0 +1,197 @@ +{ + "encoding": { + "x": { + "field": "Step", + "type": "ordinal", + "sort": null, + "axis": { + "labelAngle": -45 + } + } + }, + "transform": [ + { + "window": [ + { + "op": "row_number", + "as": "__wf_row" + } + ] + }, + { + "joinaggregate": [ + { + "op": "count", + "as": "__wf_total" + } + ] + }, + { + "calculate": "datum.__wf_row === datum.__wf_total ? 'end' : 'delta'", + "as": "__wf_type" + }, + { + "window": [ + { + "op": "sum", + "field": "Population (M)", + "as": "__wf_sum_raw" + } + ] + }, + { + "calculate": "datum['__wf_type'] === 'end' ? datum.__wf_sum_raw - datum['Population (M)'] : datum.__wf_sum_raw", + "as": "__wf_sum" + }, + { + "calculate": "datum['__wf_type'] === 'end' ? 0 : datum.__wf_sum - datum['Population (M)']", + "as": "__wf_prev_sum" + }, + { + "calculate": "(datum['__wf_type'] === 'start' || datum['__wf_type'] === 'end') ? 'total' : datum['Population (M)'] >= 0 ? 'increase' : 'decrease'", + "as": "__wf_color" + }, + { + "window": [ + { + "op": "lead", + "field": "Step", + "as": "__wf_lead" + } + ] + }, + { + "calculate": "datum.__wf_lead === null ? datum['Step'] : datum.__wf_lead", + "as": "__wf_lead" + }, + { + "calculate": "datum.__wf_lead === datum['Step'] ? null : datum.__wf_sum", + "as": "__wf_connector_y" + } + ], + "layer": [ + { + "mark": { + "type": "bar" + }, + "encoding": { + "y": { + "field": "__wf_prev_sum", + "type": "quantitative", + "title": "Population (M)", + "axis": { + "format": ",.12~g" + } + }, + "y2": { + "field": "__wf_sum" + }, + "color": { + "field": "__wf_color", + "type": "nominal", + "scale": { + "domain": [ + "total", + "increase", + "decrease" + ], + "range": [ + "#f7e0b6", + "#93c4aa", + "#f78a64" + ] + }, + "legend": { + "title": "Type" + } + } + } + }, + { + "mark": { + "type": "rule", + "color": "#6b7280", + "opacity": 0.7, + "strokeWidth": 1 + }, + "encoding": { + "x": { + "field": "Step", + "type": "ordinal", + "sort": null, + "bandPosition": 0 + }, + "x2": { + "field": "__wf_lead", + "bandPosition": 1 + }, + "y": { + "field": "__wf_connector_y", + "type": "quantitative", + "axis": { + "format": ",.12~g" + } + } + } + } + ], + "config": { + "view": { + "continuousWidth": 340, + "continuousHeight": 230 + }, + "axisX": { + "labelLimit": 100, + "labelFontSize": 10, + "titleFontSize": 11 + }, + "axisY": { + "labelFontSize": 10, + "titleFontSize": 11 + }, + "legend": { + "labelFontSize": 10, + "titleFontSize": 11 + }, + "facet": { + "spacing": 23 + } + }, + "width": { + "step": 23 + }, + "data": { + "values": [ + { + "Step": "1950", + "Population (M)": 2536 + }, + { + "Step": "Asia", + "Population (M)": 3237 + }, + { + "Step": "Africa", + "Population (M)": 1134 + }, + { + "Step": "Americas", + "Population (M)": 684 + }, + { + "Step": "Europe", + "Population (M)": 199 + }, + { + "Step": "Oceania", + "Population (M)": 32 + } + ] + }, + "title": { + "text": "Asia added more people than the world held in 1950", + "subtitle": [ + "Contribution to world population growth by region, 1950–2020, millions" + ] + } +} diff --git a/site/src/playground/theme-lab-assets/population-waterfall.mckinsey.json b/site/src/playground/theme-lab-assets/population-waterfall.mckinsey.json new file mode 100644 index 00000000..6e69fba6 --- /dev/null +++ b/site/src/playground/theme-lab-assets/population-waterfall.mckinsey.json @@ -0,0 +1,127 @@ +{ + "$schema": "https://vega-lite.github.io/schema/vega-lite/v5.json", + "__theme__": "mckinsey", + "__design__": [ + "Flint's waterfall mislabels its own last bar. Its transform marks the final row as the 'end' total, so Oceania — a 32-million contribution — is drawn as a full-height 7,790 bar in 'total' beige. The chart says the last region listed is the sum of everything before it. Here every one of the five regions is a delta and the running total is stated once, as a label, where the last bar ends.", + "The three-swatch legend deleted. Flint spends a legend on 'total / increase / decrease' when no bar in this data decreases. Two fills — navy #051C2C for the 1950 base, brand blue #2251FF for every contribution — need no key because the x labels already name them, and the distinction they draw is the one the chart type is built on: a level against the steps that move it.", + "Value axis deleted and the numbers moved onto the bars, signed. '+3,237' says contribution; the unsigned '2,536' on the first bar says level. Flint's ',.12~g' axis format — twelve significant digits on a count of millions — is gone with it.", + "The −45° rotated x labels straightened. '1950', 'Asia', 'Africa', 'Americas', 'Europe', 'Oceania' fit horizontally at a 42px step; rotation was a fallback for a width that was never measured.", + "Connectors kept but thinned to a 0.8px #d3dce1 hairline. They are scaffolding for reading the steps, not data, and Flint draws them at the same weight as the bars." + ], + "background": "#ffffff", + "padding": { "left": 8, "top": 6, "right": 10, "bottom": 8 }, + "title": { + "text": "Asia added more people than the world held in 1950", + "subtitle": ["Contribution to world population growth by region, 1950–2020, millions"] + }, + "width": 262, + "height": 200, + "data": { + "values": [ + { "Step": "1950", "Population (M)": 2536 }, + { "Step": "Asia", "Population (M)": 3237 }, + { "Step": "Africa", "Population (M)": 1134 }, + { "Step": "Americas", "Population (M)": 684 }, + { "Step": "Europe", "Population (M)": 199 }, + { "Step": "Oceania", "Population (M)": 32 } + ] + }, + "transform": [ + { "window": [{ "op": "sum", "field": "Population (M)", "as": "__cum" }], "frame": [null, 0] }, + { "calculate": "datum.__cum - datum['Population (M)']", "as": "__prev" }, + { "window": [{ "op": "lead", "field": "Step", "as": "__lead" }] }, + { "calculate": "datum.__lead === null ? null : datum.__cum", "as": "__connY" }, + { + "calculate": "datum.Step === '1950' ? format(datum['Population (M)'], ',') : '+' + format(datum['Population (M)'], ',')", + "as": "__label" + } + ], + "encoding": { + "x": { + "field": "Step", + "type": "ordinal", + "sort": null, + "title": null, + "scale": { "paddingInner": 0.35 }, + "axis": { "labelAngle": 0, "domain": false, "ticks": false, "labelPadding": 4 } + } + }, + "layer": [ + { + "mark": { "type": "rule", "color": "#d3dce1", "strokeWidth": 0.8 }, + "encoding": { + "x": { "field": "Step", "type": "ordinal", "sort": null, "bandPosition": 1 }, + "x2": { "field": "__lead", "bandPosition": 0 }, + "y": { "field": "__connY", "type": "quantitative", "axis": null, "scale": { "domain": [0, 8600], "nice": false } } + } + }, + { + "mark": { "type": "bar", "width": { "band": 0.78 } }, + "encoding": { + "y": { + "field": "__prev", + "type": "quantitative", + "title": null, + "axis": null, + "scale": { "domain": [0, 8600], "nice": false } + }, + "y2": { "field": "__cum" }, + "color": { + "condition": [ + { "test": "datum.Step === '1950'", "value": "#051c2c" } + ], + "value": "#2251ff" + } + } + }, + { + "mark": { + "type": "text", + "align": "center", + "baseline": "bottom", + "dy": -4, + "font": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "fontSize": 10, + "fontWeight": 600 + }, + "encoding": { + "y": { "field": "__cum", "type": "quantitative", "axis": null, "scale": { "domain": [0, 8600], "nice": false } }, + "text": { "field": "__label", "type": "nominal" }, + "color": { + "condition": [ + { "test": "datum.Step === '1950'", "value": "#051c2c" } + ], + "value": "#2251ff" + } + } + } + ], + "config": { + "background": "#ffffff", + "font": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "title": { + "font": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "fontSize": 14, + "fontWeight": 700, + "color": "#051c2c", + "anchor": "start", + "offset": 12, + "subtitleFont": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "subtitleFontSize": 10.5, + "subtitleColor": "#5c6b75", + "subtitlePadding": 6 + }, + "view": { "stroke": null }, + "axis": { + "labelFont": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "labelFontSize": 10, + "labelColor": "#051c2c", + "titleFont": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "titleFontSize": 9, + "titleColor": "#8fa0ab", + "grid": false, + "domain": false, + "ticks": false + } + } +} diff --git a/site/src/playground/theme-lab-assets/population.flint.json b/site/src/playground/theme-lab-assets/population.flint.json new file mode 100644 index 00000000..646bbfe4 --- /dev/null +++ b/site/src/playground/theme-lab-assets/population.flint.json @@ -0,0 +1,95 @@ +{ + "mark": "bar", + "encoding": { + "y": { + "field": "Country", + "type": "nominal", + "sort": null + }, + "x": { + "field": "Population", + "type": "quantitative", + "scale": { + "zero": true + }, + "axis": { + "format": ",.12~g" + } + } + }, + "config": { + "view": { + "continuousWidth": 280, + "continuousHeight": 230 + }, + "axisX": { + "labelLimit": 100, + "labelFontSize": 10, + "titleFontSize": 11 + }, + "axisY": { + "labelFontSize": 10, + "titleFontSize": 11 + }, + "legend": { + "labelFontSize": 10, + "titleFontSize": 11 + }, + "facet": { + "spacing": 23 + } + }, + "height": { + "step": 23 + }, + "data": { + "values": [ + { + "Country": "India", + "Population": 1428.6 + }, + { + "Country": "China", + "Population": 1425.7 + }, + { + "Country": "United States", + "Population": 339.9 + }, + { + "Country": "Indonesia", + "Population": 277.5 + }, + { + "Country": "Pakistan", + "Population": 240.5 + }, + { + "Country": "Nigeria", + "Population": 223.8 + }, + { + "Country": "Brazil", + "Population": 216.4 + }, + { + "Country": "Bangladesh", + "Population": 173 + }, + { + "Country": "Russia", + "Population": 144.4 + }, + { + "Country": "Mexico", + "Population": 128.5 + } + ] + }, + "title": { + "text": "Most populous countries, 2023", + "subtitle": [ + "Population in millions" + ] + } +} diff --git a/site/src/playground/theme-lab-assets/population.mckinsey.json b/site/src/playground/theme-lab-assets/population.mckinsey.json new file mode 100644 index 00000000..cfedd310 --- /dev/null +++ b/site/src/playground/theme-lab-assets/population.mckinsey.json @@ -0,0 +1,109 @@ +{ + "$schema": "https://vega-lite.github.io/schema/vega-lite/v5.json", + "__theme__": "mckinsey", + "__design__": [ + "Value axis deleted (axis: null) and replaced by direct value labels at the end of each bar. This is the single biggest structural change: the reader never traverses to an axis.", + "One measure, one colour: every bar in brand blue #2251FF, every value label in navy #051C2C. Population is already fully encoded by length, so a second colour treatment would be the same number stated twice.", + "Bars flipped to horizontal, keeping the baseline's row order; country labels read left-to-right at 10pt instead of Flint's rotated x labels.", + "All gridlines, ticks, domains and axis titles removed. What remains is category label → bar → number.", + "X scale domain padded to 1650 so the value labels have room to sit outside the bars rather than colliding with the plot edge.", + "Bar height cut to 0.62 of the band and ink switched to deep navy #051C2C — the deck grid wants whitespace more than it wants mark area." + ], + "background": "#ffffff", + "padding": { "left": 8, "top": 6, "right": 26, "bottom": 8 }, + "title": { + "text": "Most populous countries, 2023", + "subtitle": ["Population in millions"] + }, + "width": 250, + "height": 210, + "data": { + "values": [ + { "Country": "India", "Population": 1428.6 }, + { "Country": "China", "Population": 1425.7 }, + { "Country": "United States", "Population": 339.9 }, + { "Country": "Indonesia", "Population": 277.5 }, + { "Country": "Pakistan", "Population": 240.5 }, + { "Country": "Nigeria", "Population": 223.8 }, + { "Country": "Brazil", "Population": 216.4 }, + { "Country": "Bangladesh", "Population": 173 }, + { "Country": "Russia", "Population": 144.4 }, + { "Country": "Mexico", "Population": 128.5 } + ] + }, + "encoding": { + "y": { + "field": "Country", + "type": "nominal", + "sort": [ + "India", + "China", + "United States", + "Indonesia", + "Pakistan", + "Nigeria", + "Brazil", + "Bangladesh", + "Russia", + "Mexico" + ], + "title": null, + "axis": { "domain": false, "ticks": false, "grid": false, "labelPadding": 6 } + }, + "x": { + "field": "Population", + "type": "quantitative", + "title": null, + "scale": { "zero": true, "domain": [0, 1650] }, + "axis": null + } + }, + "layer": [ + { + "mark": { "type": "bar", "height": { "band": 0.62 }, "color": "#2251ff" } + }, + { + "mark": { + "type": "text", + "align": "left", + "baseline": "middle", + "dx": 5, + "font": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "fontSize": 10, + "fontWeight": 600, + "color": "#051c2c" + }, + "encoding": { + "text": { "field": "Population", "type": "quantitative", "format": ",.0f" } + } + } + ], + "config": { + "background": "#ffffff", + "font": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "title": { + "font": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "fontSize": 14, + "fontWeight": 700, + "color": "#051c2c", + "anchor": "start", + "offset": 12, + "subtitleFont": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "subtitleFontSize": 10.5, + "subtitleColor": "#5c6b75", + "subtitlePadding": 6 + }, + "view": { "stroke": null }, + "axis": { + "labelFont": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "labelFontSize": 10, + "labelColor": "#051c2c", + "titleFont": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "titleFontSize": 9, + "titleColor": "#8fa0ab", + "grid": false, + "domain": false, + "ticks": false + } + } +} diff --git a/site/src/playground/theme-lab-assets/release-gantt.flint.json b/site/src/playground/theme-lab-assets/release-gantt.flint.json new file mode 100644 index 00000000..23032dce --- /dev/null +++ b/site/src/playground/theme-lab-assets/release-gantt.flint.json @@ -0,0 +1,90 @@ +{ + "mark": { + "type": "bar", + "cornerRadius": 2, + "height": { + "band": 0.7 + } + }, + "encoding": { + "y": { + "field": "Task", + "type": "nominal", + "sort": { + "field": "Start", + "op": "min", + "order": "ascending" + }, + "axis": { + "title": null + } + }, + "x": { + "field": "Start", + "type": "temporal", + "axis": { + "title": null + } + }, + "x2": { + "field": "End" + } + }, + "config": { + "view": { + "continuousWidth": 280, + "continuousHeight": 230 + }, + "axisX": { + "labelLimit": 100, + "labelFontSize": 10, + "titleFontSize": 11 + }, + "axisY": { + "labelFontSize": 10, + "titleFontSize": 11 + }, + "legend": { + "labelFontSize": 10, + "titleFontSize": 11 + }, + "facet": { + "spacing": 23 + } + }, + "height": { + "step": 23 + }, + "data": { + "values": [ + { + "Task": "Planning", + "Start": "2024-01-01", + "End": "2024-01-14" + }, + { + "Task": "Design", + "Start": "2024-01-15", + "End": "2024-02-04" + }, + { + "Task": "Implementation", + "Start": "2024-02-05", + "End": "2024-03-17" + }, + { + "Task": "Testing", + "Start": "2024-03-11", + "End": "2024-04-07" + }, + { + "Task": "Launch", + "Start": "2024-04-08", + "End": "2024-04-15" + } + ] + }, + "title": { + "text": "Software release schedule" + } +} diff --git a/site/src/playground/theme-lab-assets/renewable-bullet.flint.json b/site/src/playground/theme-lab-assets/renewable-bullet.flint.json new file mode 100644 index 00000000..15f46f08 --- /dev/null +++ b/site/src/playground/theme-lab-assets/renewable-bullet.flint.json @@ -0,0 +1,285 @@ +{ + "encoding": { + "y": { + "field": "Country", + "type": "nominal", + "sort": null, + "axis": { + "title": null + } + } + }, + "layer": [ + { + "data": { + "values": [ + { + "Country": "Norway", + "__lo": 0, + "__hi": 25 + }, + { + "Country": "Brazil", + "__lo": 0, + "__hi": 23.75 + }, + { + "Country": "Germany", + "__lo": 0, + "__hi": 20 + }, + { + "Country": "World", + "__lo": 0, + "__hi": 15 + }, + { + "Country": "United States", + "__lo": 0, + "__hi": 12.5 + } + ] + }, + "mark": { + "type": "rect", + "color": "#e2e2e2", + "opacity": 1 + }, + "encoding": { + "x": { + "field": "__lo", + "type": "quantitative", + "axis": { + "title": "Share", + "format": ",.12~g" + } + }, + "x2": { + "field": "__hi" + } + } + }, + { + "data": { + "values": [ + { + "Country": "Norway", + "__lo": 25, + "__hi": 50 + }, + { + "Country": "Brazil", + "__lo": 23.75, + "__hi": 47.5 + }, + { + "Country": "Germany", + "__lo": 20, + "__hi": 40 + }, + { + "Country": "World", + "__lo": 15, + "__hi": 30 + }, + { + "Country": "United States", + "__lo": 12.5, + "__hi": 25 + } + ] + }, + "mark": { + "type": "rect", + "color": "#ececec", + "opacity": 1 + }, + "encoding": { + "x": { + "field": "__lo", + "type": "quantitative", + "axis": { + "title": "Share", + "format": ",.12~g" + } + }, + "x2": { + "field": "__hi" + } + } + }, + { + "data": { + "values": [ + { + "Country": "Norway", + "__lo": 50, + "__hi": 75 + }, + { + "Country": "Brazil", + "__lo": 47.5, + "__hi": 71.25 + }, + { + "Country": "Germany", + "__lo": 40, + "__hi": 60 + }, + { + "Country": "World", + "__lo": 30, + "__hi": 45 + }, + { + "Country": "United States", + "__lo": 25, + "__hi": 37.5 + } + ] + }, + "mark": { + "type": "rect", + "color": "#f5f5f5", + "opacity": 1 + }, + "encoding": { + "x": { + "field": "__lo", + "type": "quantitative", + "axis": { + "title": "Share", + "format": ",.12~g" + } + }, + "x2": { + "field": "__hi" + } + } + }, + { + "mark": { + "type": "bar", + "height": { + "band": 0.5 + } + }, + "encoding": { + "x": { + "field": "Share", + "type": "quantitative", + "scale": { + "zero": true + }, + "axis": { + "title": "Share", + "format": ",.12~g" + } + }, + "color": { + "field": "__status", + "type": "nominal", + "scale": { + "domain": [ + "Below target", + "Meets target" + ], + "range": [ + "#c44e52", + "#2f855a" + ] + }, + "legend": { + "title": null + }, + "title": null + } + }, + "transform": [ + { + "calculate": "datum[\"Share\"] >= datum[\"Target\"] ? 'Meets target' : 'Below target'", + "as": "__status" + } + ] + }, + { + "mark": { + "type": "tick", + "color": "#1a1a1a", + "thickness": 3, + "opacity": 1, + "size": 17 + }, + "encoding": { + "x": { + "field": "Target", + "type": "quantitative", + "axis": { + "title": "Share", + "format": ",.12~g" + } + } + } + } + ], + "config": { + "view": { + "continuousWidth": 280, + "continuousHeight": 230 + }, + "axisX": { + "labelLimit": 100, + "labelFontSize": 10, + "titleFontSize": 11 + }, + "axisY": { + "labelFontSize": 10, + "titleFontSize": 11 + }, + "legend": { + "labelFontSize": 10, + "titleFontSize": 11 + }, + "facet": { + "spacing": 23 + } + }, + "height": { + "step": 23 + }, + "data": { + "values": [ + { + "Country": "Norway", + "Share": 98.6, + "Target": 100 + }, + { + "Country": "Brazil", + "Share": 89.2, + "Target": 95 + }, + { + "Country": "Germany", + "Share": 51.6, + "Target": 80 + }, + { + "Country": "World", + "Share": 30.3, + "Target": 60 + }, + { + "Country": "United States", + "Share": 22.7, + "Target": 50 + } + ] + }, + "title": { + "text": "Every country is short of its renewable target", + "subtitle": [ + "Renewable share of electricity, 2023, per cent, against national targets" + ] + } +} diff --git a/site/src/playground/theme-lab-assets/renewable-bullet.powerbi.json b/site/src/playground/theme-lab-assets/renewable-bullet.powerbi.json new file mode 100644 index 00000000..ce66c876 --- /dev/null +++ b/site/src/playground/theme-lab-assets/renewable-bullet.powerbi.json @@ -0,0 +1,118 @@ +{ + "$schema": "https://vega-lite.github.io/schema/vega-lite/v5.json", + "__theme__": "powerbi", + "__design__": [ + "Flint's qualitative bands are not comparable across rows. It hardcodes five separate datasets in which each country's three grey bands are 25%, 50% and 75% of that country's own target — so on a shared 0–100 axis Norway's first band ends at 25 and America's ends at 12.5. Identical greys sit at different absolute positions and mean different things. Replaced by one track running 0–100, which is the actual range of a percentage and is the same on every row.", + "The legend deleted. Flint offers 'Below target' in red and 'Meets target' in green when not one of the five countries meets its target — a two-item key of which one item never appears, spending a colour on an empty set.", + "The shortfall stated as a number at the end of each row, in #E66C37. This is what the chart is for, and Flint asks the reader to measure the distance from the bar end to the tick by eye.", + "Bars in brand #118DFF, target ticks in #F3F2F1 at 2px. Flint's red bars pre-judge every row as a failure before the reader has looked at the gap; the theme reserves status colour for the threshold statement, and here it appears once, on the number.", + "Descending order pinned in the spec rather than depending on 'sort: null' and the order the rows happen to arrive in. The rendered sequence is the baseline's; only its robustness changes.", + "Inverted to the #1B1A19 report canvas with 9.5pt Segoe UI, dashed #3B3A39 gridlines at low contrast, and Flint's ',.12~g' axis format replaced by integers with a per-cent suffix." + ], + "background": "#1b1a19", + "padding": { "left": 10, "top": 6, "right": 14, "bottom": 8 }, + "title": { + "text": "Every country is short of its renewable target", + "subtitle": ["Renewable share of electricity, 2023, per cent, against national targets"] + }, + "width": 260, + "height": 150, + "data": { + "values": [ + { "Country": "Norway", "Share": 98.6, "Target": 100 }, + { "Country": "Brazil", "Share": 89.2, "Target": 95 }, + { "Country": "Germany", "Share": 51.6, "Target": 80 }, + { "Country": "World", "Share": 30.3, "Target": 60 }, + { "Country": "United States", "Share": 22.7, "Target": 50 } + ] + }, + "transform": [ + { "calculate": "'−' + format(datum.Target - datum.Share, '.1f')", "as": "__gapLabel" } + ], + "encoding": { + "y": { + "field": "Country", + "type": "nominal", + "title": null, + "sort": { "field": "Share", "op": "max", "order": "descending" }, + "scale": { "paddingInner": 0.45 }, + "axis": { "grid": false, "domain": false, "ticks": false, "labelColor": "#f3f2f1", "labelPadding": 6 } + }, + "x": { + "field": "Share", + "type": "quantitative", + "title": null, + "scale": { "zero": true, "nice": false, "domain": [0, 104] }, + "axis": { + "values": [0, 25, 50, 75, 100], + "labelExpr": "datum.value + '%'", + "grid": true, + "gridColor": "#3b3a39", + "gridDash": [3, 3], + "domain": false, + "ticks": false + } + } + }, + "layer": [ + { + "mark": { "type": "bar", "fill": "#2b2a29", "height": { "band": 1 } }, + "encoding": { + "x": { "datum": 0 }, + "x2": { "datum": 100 } + } + }, + { + "mark": { "type": "bar", "fill": "#118dff", "height": { "band": 1 } } + }, + { + "mark": { "type": "tick", "color": "#f3f2f1", "thickness": 2, "size": 20 }, + "encoding": { + "x": { "field": "Target", "type": "quantitative" } + } + }, + { + "mark": { + "type": "text", + "align": "left", + "baseline": "middle", + "dx": 8, + "fontSize": 10, + "fontWeight": 600, + "color": "#e66c37" + }, + "encoding": { + "x": { "field": "Target", "type": "quantitative" }, + "text": { "field": "__gapLabel", "type": "nominal" } + } + } + ], + "config": { + "background": "#1b1a19", + "font": "'Segoe UI', 'Helvetica Neue', Arial, sans-serif", + "title": { + "font": "'Segoe UI', 'Helvetica Neue', Arial, sans-serif", + "fontSize": 12.5, + "fontWeight": 600, + "color": "#f3f2f1", + "anchor": "start", + "offset": 10, + "subtitleFont": "'Segoe UI', 'Helvetica Neue', Arial, sans-serif", + "subtitleFontSize": 9.5, + "subtitleColor": "#8a8886", + "subtitlePadding": 6 + }, + "view": { "stroke": null }, + "axis": { + "labelFont": "'Segoe UI', 'Helvetica Neue', Arial, sans-serif", + "labelFontSize": 9.5, + "labelColor": "#8a8886", + "titleFont": "'Segoe UI', 'Helvetica Neue', Arial, sans-serif", + "titleFontSize": 9.5, + "titleColor": "#8a8886", + "grid": false, + "domain": false, + "ticks": false + } + } +} diff --git a/site/src/playground/theme-lab-assets/renewable-kpi.flint.json b/site/src/playground/theme-lab-assets/renewable-kpi.flint.json new file mode 100644 index 00000000..bc5e6ab5 --- /dev/null +++ b/site/src/playground/theme-lab-assets/renewable-kpi.flint.json @@ -0,0 +1,208 @@ +{ + "layer": [ + { + "data": { + "values": [ + {} + ] + }, + "mark": { + "type": "rect", + "fill": "#ffffff", + "stroke": "#e6e9ef", + "strokeWidth": 1, + "cornerRadius": 8, + "tooltip": null + }, + "encoding": { + "x": { + "value": 13 + }, + "x2": { + "value": 327 + }, + "y": { + "value": 14 + }, + "y2": { + "value": 229 + } + } + }, + { + "data": { + "values": [ + {} + ] + }, + "mark": { + "type": "text", + "fontSize": 22, + "fontWeight": 500, + "fill": "#4a4a4a", + "align": "center", + "baseline": "top", + "text": "Renewable share", + "tooltip": null + }, + "encoding": { + "x": { + "value": 170 + }, + "y": { + "value": 45 + } + } + }, + { + "data": { + "values": [ + {} + ] + }, + "mark": { + "type": "text", + "fontSize": 68, + "fontWeight": "bold", + "fill": "#1a1a1a", + "align": "center", + "baseline": "middle", + "text": "30.3", + "tooltip": null + }, + "encoding": { + "x": { + "value": 170 + }, + "y": { + "value": 113 + } + } + }, + { + "data": { + "values": [ + {} + ] + }, + "mark": { + "type": "text", + "fontSize": 18, + "fontWeight": 400, + "fill": "#666", + "align": "center", + "baseline": "top", + "text": "67% of 45", + "tooltip": null + }, + "encoding": { + "x": { + "value": 170 + }, + "y": { + "value": 165 + } + } + }, + { + "data": { + "values": [ + {} + ] + }, + "mark": { + "type": "rect", + "fill": "#e6e9ef", + "cornerRadius": 3.5, + "tooltip": null + }, + "encoding": { + "x": { + "value": 34 + }, + "x2": { + "value": 306 + }, + "y": { + "value": 192 + }, + "y2": { + "value": 199 + } + } + }, + { + "data": { + "values": [ + {} + ] + }, + "mark": { + "type": "rect", + "fill": "#5b8def", + "cornerRadius": 3.5, + "tooltip": null + }, + "encoding": { + "x": { + "value": 34 + }, + "x2": { + "value": 217.14666666666668 + }, + "y": { + "value": 192 + }, + "y2": { + "value": 199 + } + } + } + ], + "width": 340, + "height": 243, + "resolve": { + "scale": { + "x": "independent", + "y": "independent" + } + }, + "config": { + "view": { + "continuousWidth": 340, + "continuousHeight": 243, + "stroke": null + }, + "axisX": { + "labelLimit": 100, + "labelFontSize": 10, + "titleFontSize": 11 + }, + "axisY": { + "labelFontSize": 10, + "titleFontSize": 11 + }, + "legend": { + "labelFontSize": 10, + "titleFontSize": 11 + }, + "facet": { + "spacing": 23 + } + }, + "data": { + "values": [ + { + "Metric": "Renewable share", + "Share (%)": 30.3, + "Target": 45 + } + ] + }, + "title": { + "text": "Renewables supply 30% of the world's electricity", + "subtitle": [ + "Share of global electricity generation, 2023, against a 45% target" + ] + } +} diff --git a/site/src/playground/theme-lab-assets/renewable-kpi.powerbi.json b/site/src/playground/theme-lab-assets/renewable-kpi.powerbi.json new file mode 100644 index 00000000..e31d0c03 --- /dev/null +++ b/site/src/playground/theme-lab-assets/renewable-kpi.powerbi.json @@ -0,0 +1,191 @@ +{ + "$schema": "https://vega-lite.github.io/schema/vega-lite/v5.json", + "__theme__": "powerbi", + "__design__": [ + "The data is out of the geometry. Flint draws the progress bar as a rect ending at x = 217.14666666666668 — a pixel coordinate with the arithmetic 30.3 ÷ 45 × 272 already performed and baked in. Change the number in the dataset and the bar does not move. Here the bar is a rect on a real 0–50 scale driven by the field.", + "'67% of 45' is a string literal in Flint's spec, not a computed label. Same failure, worse consequence: it is the only place the target appears, and it will keep saying 67% after the data changes. It is recomputed here from Share and Target, alongside the gap in points.", + "The white card deleted. A #FFFFFF panel with #1A1A1A type is a hole punched in a #1B1A19 report page; the tile now sits on the canvas and takes its contrast from the type, not from an inverted surface.", + "The target is now on the chart, as a tick at 45 with its own label, instead of surviving only inside a sentence. A progress bar with no visible goal is a bar with no scale.", + "Status colour used where the theme reserves it — for a threshold. The fill is brand #118DFF because the measure itself is neutral; the shortfall is #E66C37 because the value is under target. Flint's #5B8DEF says nothing either way.", + "The metric name comes from the Metric field rather than being retyped as a mark literal, so the tile is a template rather than a drawing.", + "68pt display type cut to 40pt Segoe UI semibold. A KPI number has to be the largest thing on the tile, not the largest thing on the page." + ], + "background": "#1b1a19", + "padding": { "left": 10, "top": 6, "right": 12, "bottom": 8 }, + "title": { + "text": "Renewables supply 30% of the world's electricity", + "subtitle": ["Share of global electricity generation, 2023, against a 45% target"] + }, + "data": { + "values": [{ "Metric": "Renewable share", "Share (%)": 30.3, "Target": 45 }] + }, + "transform": [ + { "calculate": "format(datum['Share (%)'], '.1f') + '%'", "as": "__big" }, + { + "calculate": "format(datum['Share (%)'] / datum.Target, '.0%') + ' of target'", + "as": "__pctLabel" + }, + { + "calculate": "format(datum.Target - datum['Share (%)'], '.1f') + ' points short'", + "as": "__gapLabel" + } + ], + "spacing": 6, + "vconcat": [ + { + "width": 300, + "height": 74, + "layer": [ + { + "mark": { + "type": "text", + "align": "left", + "baseline": "top", + "fontSize": 11, + "fontWeight": 600, + "color": "#b3b0ad" + }, + "encoding": { + "x": { "value": 0 }, + "y": { "value": 0 }, + "text": { "field": "Metric", "type": "nominal" } + } + }, + { + "mark": { + "type": "text", + "align": "left", + "baseline": "middle", + "fontSize": 40, + "fontWeight": 600, + "color": "#f3f2f1" + }, + "encoding": { + "x": { "value": 0 }, + "y": { "value": 42 }, + "text": { "field": "__big", "type": "nominal" } + } + }, + { + "mark": { + "type": "text", + "align": "left", + "baseline": "bottom", + "fontSize": 11, + "color": "#e66c37" + }, + "encoding": { + "x": { "value": 0 }, + "y": { "value": 74 }, + "text": { "field": "__gapLabel", "type": "nominal" } + } + }, + { + "mark": { + "type": "text", + "align": "right", + "baseline": "bottom", + "fontSize": 11, + "color": "#b3b0ad" + }, + "encoding": { + "x": { "value": 300 }, + "y": { "value": 74 }, + "text": { "field": "__pctLabel", "type": "nominal" } + } + } + ] + }, + { + "width": 300, + "height": 22, + "encoding": { + "x": { + "field": "Share (%)", + "type": "quantitative", + "title": null, + "scale": { "zero": true, "nice": false, "domain": [0, 50] }, + "axis": { + "values": [0, 10, 20, 30, 40, 50], + "labelExpr": "datum.value + '%'", + "grid": false, + "domain": false, + "ticks": false, + "labelPadding": 4 + } + } + }, + "layer": [ + { + "mark": { "type": "rect", "fill": "#3b3a39", "cornerRadius": 3 }, + "encoding": { + "x": { "datum": 0 }, + "x2": { "datum": 50 }, + "y": { "value": 4 }, + "y2": { "value": 16 } + } + }, + { + "mark": { "type": "rect", "fill": "#118dff", "cornerRadius": 3 }, + "encoding": { + "x": { "datum": 0 }, + "x2": { "field": "Share (%)" }, + "y": { "value": 4 }, + "y2": { "value": 16 } + } + }, + { + "mark": { "type": "rule", "stroke": "#f3f2f1", "strokeWidth": 2 }, + "encoding": { + "x": { "field": "Target", "type": "quantitative" }, + "y": { "value": 0 }, + "y2": { "value": 20 } + } + }, + { + "mark": { + "type": "text", + "align": "center", + "baseline": "bottom", + "dy": -2, + "fontSize": 9.5, + "color": "#f3f2f1" + }, + "encoding": { + "x": { "field": "Target", "type": "quantitative" }, + "y": { "value": 0 }, + "text": { "value": "Target" } + } + } + ] + } + ], + "config": { + "background": "#1b1a19", + "font": "'Segoe UI', 'Helvetica Neue', Arial, sans-serif", + "title": { + "font": "'Segoe UI', 'Helvetica Neue', Arial, sans-serif", + "fontSize": 12.5, + "fontWeight": 600, + "color": "#f3f2f1", + "anchor": "start", + "offset": 10, + "subtitleFont": "'Segoe UI', 'Helvetica Neue', Arial, sans-serif", + "subtitleFontSize": 9.5, + "subtitleColor": "#8a8886", + "subtitlePadding": 6 + }, + "view": { "stroke": null }, + "axis": { + "labelFont": "'Segoe UI', 'Helvetica Neue', Arial, sans-serif", + "labelFontSize": 9.5, + "labelColor": "#8a8886", + "titleFont": "'Segoe UI', 'Helvetica Neue', Arial, sans-serif", + "titleFontSize": 9.5, + "titleColor": "#8a8886", + "grid": false, + "domain": false, + "ticks": false + } + } +} diff --git a/site/src/playground/theme-lab-assets/renewables-projection.flint.json b/site/src/playground/theme-lab-assets/renewables-projection.flint.json new file mode 100644 index 00000000..d0d003e0 --- /dev/null +++ b/site/src/playground/theme-lab-assets/renewables-projection.flint.json @@ -0,0 +1,101 @@ +{ + "mark": "line", + "encoding": { + "x": { + "field": "Year", + "type": "temporal" + }, + "y": { + "field": "Capacity (GW)", + "type": "quantitative", + "scale": { + "zero": true + }, + "axis": { + "format": ",.12~g" + } + }, + "strokeDash": { + "field": "Series", + "type": "nominal", + "sort": null + } + }, + "config": { + "view": { + "continuousWidth": 308, + "continuousHeight": 254 + }, + "axisX": { + "labelLimit": 100, + "labelFontSize": 10, + "titleFontSize": 11 + }, + "axisY": { + "labelFontSize": 10, + "titleFontSize": 11 + }, + "legend": { + "labelFontSize": 10, + "titleFontSize": 11 + }, + "facet": { + "spacing": 25 + } + }, + "data": { + "values": [ + { + "Year": "2015", + "Series": "Observed", + "Capacity (GW)": 785 + }, + { + "Year": "2017", + "Series": "Observed", + "Capacity (GW)": 1080 + }, + { + "Year": "2019", + "Series": "Observed", + "Capacity (GW)": 1440 + }, + { + "Year": "2021", + "Series": "Observed", + "Capacity (GW)": 1900 + }, + { + "Year": "2023", + "Series": "Observed", + "Capacity (GW)": 2560 + }, + { + "Year": "2023", + "Series": "Projected", + "Capacity (GW)": 2560 + }, + { + "Year": "2025", + "Series": "Projected", + "Capacity (GW)": 3400 + }, + { + "Year": "2027", + "Series": "Projected", + "Capacity (GW)": 4300 + }, + { + "Year": "2030", + "Series": "Projected", + "Capacity (GW)": 5800 + } + ] + }, + "title": { + "text": "Global renewable capacity", + "subtitle": [ + "Gigawatts installed, observed to 2023 and projected to 2030" + ] + } +} diff --git a/site/src/playground/theme-lab-assets/renewables-projection.nyt.json b/site/src/playground/theme-lab-assets/renewables-projection.nyt.json new file mode 100644 index 00000000..25fcdac9 --- /dev/null +++ b/site/src/playground/theme-lab-assets/renewables-projection.nyt.json @@ -0,0 +1,154 @@ +{ + "$schema": "https://vega-lite.github.io/schema/vega-lite/v5.json", + "__theme__": "nyt", + "__design__": [ + "Dash pattern re-authored so 'Observed' is solid and 'Projected' is a [5,4] dash on the same colour. Flint's default dash scale gives both series a dash and both a legend entry, which reads as two measures rather than one measure in two states.", + "Colour held constant across the two series and the legend deleted; the difference is carried entirely by line texture, which is the convention for observed-versus-forecast.", + "Series labelled at the line ends instead of in a key, using the existing Series field. This is not annotation — it is the legend, moved onto the marks so the eye never leaves the plot.", + "Y scale forced through zero with a 0–6000 domain and thousands-suffixed labels, because a capacity total is a magnitude and a truncated axis would exaggerate the projected climb.", + "Axis reduced to a horizontal grid of #E4E4E4 hairlines with no y domain and no ticks, plus a solid black x baseline — the newspaper's standard time-series frame.", + "Y title flattened to a single unit word ('GW') sitting horizontally at the top of the axis, replacing the rotated 'Capacity (GW)'.", + "Georgia for the headline and deck, Helvetica for everything numeric; line weight raised to 2.4px with a round cap.", + "The join point at 2023 is drawn once as a filled dot so the handoff between the two states has a visible pivot instead of an ambiguous overlap." + ], + "background": "#ffffff", + "padding": { "left": 4, "top": 4, "right": 46, "bottom": 6 }, + "width": 300, + "height": 210, + "title": { + "text": "Global renewable capacity", + "subtitle": ["Gigawatts installed, observed to 2023 and projected to 2030"] + }, + "data": { + "values": [ + { "Year": "2015", "Series": "Observed", "Capacity (GW)": 785 }, + { "Year": "2017", "Series": "Observed", "Capacity (GW)": 1080 }, + { "Year": "2019", "Series": "Observed", "Capacity (GW)": 1440 }, + { "Year": "2021", "Series": "Observed", "Capacity (GW)": 1900 }, + { "Year": "2023", "Series": "Observed", "Capacity (GW)": 2560 }, + { "Year": "2023", "Series": "Projected", "Capacity (GW)": 2560 }, + { "Year": "2025", "Series": "Projected", "Capacity (GW)": 3400 }, + { "Year": "2027", "Series": "Projected", "Capacity (GW)": 4300 }, + { "Year": "2030", "Series": "Projected", "Capacity (GW)": 5800 } + ] + }, + "encoding": { + "x": { + "field": "Year", + "type": "temporal", + "title": null, + "scale": { "type": "utc" }, + "axis": { + "format": "%Y", + "values": ["2015", "2020", "2025", "2030"], + "labelFlush": true, + "grid": false, + "domain": true, + "domainColor": "#000000", + "domainWidth": 1, + "ticks": true, + "tickColor": "#000000", + "tickSize": 4, + "labelPadding": 3 + } + }, + "y": { + "field": "Capacity (GW)", + "type": "quantitative", + "title": "GW", + "scale": { "domain": [0, 6000] }, + "axis": { + "tickCount": 4, + "labelExpr": "datum.value === 0 ? '0' : datum.value / 1000 + ',000'", + "grid": true, + "gridColor": "#e4e4e4", + "gridWidth": 1, + "domain": false, + "ticks": false, + "labelPadding": 5, + "titleAngle": 0, + "titleAlign": "left", + "titleX": -4, + "titleY": -10, + "titleBaseline": "bottom" + } + } + }, + "layer": [ + { + "mark": { + "type": "line", + "color": "#1a1a1a", + "strokeWidth": 2.4, + "strokeCap": "round", + "strokeJoin": "round" + }, + "encoding": { + "strokeDash": { + "field": "Series", + "type": "nominal", + "scale": { "domain": ["Observed", "Projected"], "range": [[1, 0], [5, 4]] }, + "legend": null + } + } + }, + { + "transform": [ + { "filter": "datum.Series === 'Observed'" }, + { "window": [{ "op": "row_number", "as": "__rank" }], "sort": [{ "field": "Year", "order": "descending" }] }, + { "filter": "datum.__rank === 1" } + ], + "mark": { + "type": "point", + "filled": true, + "size": 55, + "color": "#1a1a1a" + } + }, + { + "transform": [ + { "window": [{ "op": "row_number", "as": "__rank" }], "groupby": ["Series"], "sort": [{ "field": "Year", "order": "descending" }] }, + { "filter": "datum.__rank === 1" } + ], + "mark": { + "type": "text", + "align": "left", + "baseline": "middle", + "dx": 7, + "font": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "fontSize": 10, + "fontWeight": 600, + "color": "#1a1a1a" + }, + "encoding": { + "text": { "field": "Series", "type": "nominal" } + } + } + ], + "config": { + "background": "#ffffff", + "font": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "title": { + "font": "Georgia, 'Times New Roman', serif", + "fontSize": 16, + "fontWeight": 700, + "color": "#121212", + "anchor": "start", + "offset": 12, + "subtitleFont": "Georgia, 'Times New Roman', serif", + "subtitleFontSize": 11, + "subtitleColor": "#666666", + "subtitlePadding": 8 + }, + "view": { "stroke": null }, + "axis": { + "labelFont": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "labelFontSize": 10, + "labelColor": "#666666", + "titleFont": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "titleFontSize": 10, + "titleColor": "#666666", + "titleFontWeight": 400 + } + } +} diff --git a/site/src/playground/theme-lab-assets/seattle-range.economist.json b/site/src/playground/theme-lab-assets/seattle-range.economist.json new file mode 100644 index 00000000..616452dc --- /dev/null +++ b/site/src/playground/theme-lab-assets/seattle-range.economist.json @@ -0,0 +1,160 @@ +{ + "$schema": "https://vega-lite.github.io/schema/vega-lite/v5.json", + "__theme__": "economist", + "__design__": [ + "The two edges of the band named directly at December. Flint draws a translucent ribbon with no legend at all — nothing on the page says which boundary is the daily high and which is the low, so the chart is literally unreadable without the field names.", + "Opacity raised from 0.5 to a solid house blue at 0.22 and both edges of the band outlined at 1.2px. A half-transparent fill was standing in for a border; and Vega-Lite's area `line` property outlines only the y boundary, so the baseline's ribbon has a drawn low edge and an undrawn high one — the two curves have to be layered explicitly to get a band with a defined top and bottom.", + "The house 'value axis on top' rule deliberately not applied. It exists so that a reader of a horizontal bar chart meets the unit before the bars; on a vertical chart the value axis is y, and moving it to the top turns the gridlines through ninety degrees and makes the scale unreadable. A house style is a set of reasons, not a set of coordinates.", + "Degree sign moved onto the tick labels (°F) and the axis title deleted; stating a unit twice is a habit, not a design.", + "Month axis reduced to a hard black baseline with no ticks or domain elsewhere, and the band given a monotone interpolation so a twelve-point seasonal curve does not read as eleven straight decisions.", + "Red masthead rule, 13.5pt bold headline over a grey deck, 9.5pt labels: the same fixed identity every chart in the section gets." + ], + "background": "#ffffff", + "padding": { "left": 6, "top": 4, "right": 22, "bottom": 6 }, + "title": { + "text": "Seattle, month by month", + "subtitle": ["Average daily high and low temperature, °F, 1991–2020 normals"] + }, + "spacing": 8, + "vconcat": [ + { + "data": { "values": [{}] }, + "mark": { "type": "rect", "fill": "#e3120b", "stroke": null }, + "width": 26, + "height": 3, + "view": { "stroke": null } + }, + { + "width": 250, + "height": 190, + "data": { + "values": [ + { "Month": "Jan", "Low": 37, "High": 47 }, + { "Month": "Feb", "Low": 37, "High": 50 }, + { "Month": "Mar", "Low": 40, "High": 54 }, + { "Month": "Apr", "Low": 43, "High": 59 }, + { "Month": "May", "Low": 48, "High": 65 }, + { "Month": "Jun", "Low": 53, "High": 70 }, + { "Month": "Jul", "Low": 56, "High": 76 }, + { "Month": "Aug", "Low": 57, "High": 77 }, + { "Month": "Sep", "Low": 53, "High": 71 }, + { "Month": "Oct", "Low": 46, "High": 60 }, + { "Month": "Nov", "Low": 40, "High": 51 }, + { "Month": "Dec", "Low": 36, "High": 46 } + ] + }, + "encoding": { + "x": { + "field": "Month", + "type": "nominal", + "sort": ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"], + "title": null, + "scale": { "padding": 0.04 }, + "axis": { + "labelAngle": 0, + "labelPadding": 6, + "domain": true, + "domainColor": "#121317", + "domainWidth": 1, + "grid": false + } + }, + "y": { + "field": "Low", + "type": "quantitative", + "title": null, + "scale": { "zero": false, "nice": false, "domain": [30, 82] }, + "axis": { + "tickCount": 5, + "format": ".0f", + "labelExpr": "datum.label + '°F'", + "grid": true, + "gridColor": "#c9d3da", + "domain": false, + "ticks": false, + "labelPadding": 4 + } + }, + "y2": { "field": "High" } + }, + "layer": [ + { + "mark": { + "type": "area", + "interpolate": "monotone", + "color": "#006ba2", + "opacity": 0.22, + "line": false + } + }, + { + "mark": { "type": "line", "interpolate": "monotone", "color": "#006ba2", "strokeWidth": 1.2 }, + "encoding": { "y": { "field": "High", "type": "quantitative" }, "y2": null } + }, + { + "mark": { "type": "line", "interpolate": "monotone", "color": "#006ba2", "strokeWidth": 1.2 }, + "encoding": { "y": { "field": "Low", "type": "quantitative" }, "y2": null } + }, + { + "transform": [{ "filter": "datum.Month === 'Dec'" }], + "mark": { + "type": "text", + "align": "left", + "baseline": "middle", + "dx": 6, + "fontSize": 9.5, + "fontWeight": 600, + "color": "#006ba2" + }, + "encoding": { + "y": { "field": "High", "type": "quantitative" }, + "y2": null, + "text": { "value": "High" } + } + }, + { + "transform": [{ "filter": "datum.Month === 'Dec'" }], + "mark": { + "type": "text", + "align": "left", + "baseline": "middle", + "dx": 6, + "fontSize": 9.5, + "fontWeight": 600, + "color": "#006ba2" + }, + "encoding": { + "y": { "field": "Low", "type": "quantitative" }, + "y2": null, + "text": { "value": "Low" } + } + } + ] + } + ], + "config": { + "background": "#ffffff", + "font": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "title": { + "font": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "fontSize": 13.5, + "fontWeight": 700, + "color": "#121317", + "anchor": "start", + "offset": 10, + "subtitleFont": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "subtitleFontSize": 10.5, + "subtitleColor": "#54585a", + "subtitlePadding": 6 + }, + "view": { "stroke": null }, + "axis": { + "labelFont": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "labelFontSize": 9.5, + "labelColor": "#54585a", + "grid": false, + "domain": false, + "ticks": false + } + } +} diff --git a/site/src/playground/theme-lab-assets/seattle-range.flint.json b/site/src/playground/theme-lab-assets/seattle-range.flint.json new file mode 100644 index 00000000..dc4c40da --- /dev/null +++ b/site/src/playground/theme-lab-assets/seattle-range.flint.json @@ -0,0 +1,138 @@ +{ + "mark": { + "type": "area", + "opacity": 0.5, + "line": { + "strokeWidth": 1 + } + }, + "encoding": { + "x": { + "field": "Month", + "type": "nominal", + "sort": [ + "Jan", + "Feb", + "Mar", + "Apr", + "May", + "Jun", + "Jul", + "Aug", + "Sep", + "Oct", + "Nov", + "Dec" + ] + }, + "y": { + "field": "Low", + "type": "quantitative", + "scale": { + "zero": false, + "nice": true + }, + "axis": { + "format": ",.12~g" + } + }, + "y2": { + "field": "High" + } + }, + "config": { + "view": { + "continuousWidth": 340, + "continuousHeight": 230 + }, + "axisX": { + "labelLimit": 100, + "labelFontSize": 10, + "titleFontSize": 11 + }, + "axisY": { + "labelFontSize": 10, + "titleFontSize": 11 + }, + "legend": { + "labelFontSize": 10, + "titleFontSize": 11 + }, + "facet": { + "spacing": 23 + } + }, + "width": { + "step": 23 + }, + "data": { + "values": [ + { + "Month": "Jan", + "Low": 37, + "High": 47 + }, + { + "Month": "Feb", + "Low": 37, + "High": 50 + }, + { + "Month": "Mar", + "Low": 40, + "High": 54 + }, + { + "Month": "Apr", + "Low": 43, + "High": 59 + }, + { + "Month": "May", + "Low": 48, + "High": 65 + }, + { + "Month": "Jun", + "Low": 53, + "High": 70 + }, + { + "Month": "Jul", + "Low": 56, + "High": 76 + }, + { + "Month": "Aug", + "Low": 57, + "High": 77 + }, + { + "Month": "Sep", + "Low": 53, + "High": 71 + }, + { + "Month": "Oct", + "Low": 46, + "High": 60 + }, + { + "Month": "Nov", + "Low": 40, + "High": 51 + }, + { + "Month": "Dec", + "Low": 36, + "High": 46 + } + ] + }, + "title": { + "text": "Seattle, month by month", + "subtitle": [ + "Average daily high and low temperature, °F, 1991–2020 normals" + ] + } +} diff --git a/site/src/playground/theme-lab-assets/seattle-rose.flint.json b/site/src/playground/theme-lab-assets/seattle-rose.flint.json new file mode 100644 index 00000000..8e257c19 --- /dev/null +++ b/site/src/playground/theme-lab-assets/seattle-rose.flint.json @@ -0,0 +1,172 @@ +{ + "encoding": { + "theta": { + "field": "Month", + "type": "nominal", + "sort": [ + "Jan", + "Feb", + "Mar", + "Apr", + "May", + "Jun", + "Jul", + "Aug", + "Sep", + "Oct", + "Nov", + "Dec" + ], + "stack": true + } + }, + "layer": [ + { + "mark": { + "type": "arc", + "stroke": "white", + "padAngle": 0.02 + }, + "encoding": { + "radius": { + "field": "Rainfall (mm)", + "type": "quantitative", + "scale": { + "type": "sqrt" + } + }, + "color": { + "field": "Month", + "type": "nominal", + "sort": [ + "Jan", + "Feb", + "Mar", + "Apr", + "May", + "Jun", + "Jul", + "Aug", + "Sep", + "Oct", + "Nov", + "Dec" + ] + } + } + }, + { + "mark": { + "type": "text", + "radiusOffset": 15, + "fontSize": 11 + }, + "encoding": { + "text": { + "field": "Month", + "type": "nominal" + }, + "radius": { + "field": "Rainfall (mm)", + "type": "quantitative", + "scale": { + "type": "sqrt" + } + } + }, + "transform": [ + { + "aggregate": [ + { + "op": "sum", + "field": "Rainfall (mm)", + "as": "Rainfall (mm)" + } + ], + "groupby": [ + "Month" + ] + } + ] + } + ], + "width": 230, + "height": 230, + "config": { + "view": { + "continuousWidth": 230, + "continuousHeight": 230 + }, + "axisX": { + "labelLimit": 100, + "labelFontSize": 10, + "titleFontSize": 11 + }, + "axisY": { + "labelFontSize": 10, + "titleFontSize": 11 + }, + "legend": { + "labelFontSize": 10, + "titleFontSize": 11 + }, + "facet": { + "spacing": 23 + } + }, + "data": { + "values": [ + { + "Month": "Jan", + "Rainfall (mm)": 140 + }, + { + "Month": "Feb", + "Rainfall (mm)": 90 + }, + { + "Month": "Mar", + "Rainfall (mm)": 95 + }, + { + "Month": "Apr", + "Rainfall (mm)": 70 + }, + { + "Month": "May", + "Rainfall (mm)": 50 + }, + { + "Month": "Jun", + "Rainfall (mm)": 40 + }, + { + "Month": "Jul", + "Rainfall (mm)": 18 + }, + { + "Month": "Aug", + "Rainfall (mm)": 25 + }, + { + "Month": "Sep", + "Rainfall (mm)": 40 + }, + { + "Month": "Oct", + "Rainfall (mm)": 100 + }, + { + "Month": "Nov", + "Rainfall (mm)": 165 + }, + { + "Month": "Dec", + "Rainfall (mm)": 155 + } + ] + }, + "title": { + "text": "Seattle monthly rainfall (mm)" + } +} diff --git a/site/src/playground/theme-lab-assets/spending-quintile.flint.json b/site/src/playground/theme-lab-assets/spending-quintile.flint.json new file mode 100644 index 00000000..003a4cb0 --- /dev/null +++ b/site/src/playground/theme-lab-assets/spending-quintile.flint.json @@ -0,0 +1,196 @@ +{ + "mark": "bar", + "encoding": { + "x": { + "field": "Quintile", + "type": "nominal", + "sort": null + }, + "y": { + "field": "Spending ($)", + "type": "quantitative", + "stack": "normalize", + "scale": { + "zero": true + }, + "axis": { + "format": ",.12~g" + } + }, + "color": { + "field": "Category", + "type": "nominal", + "sort": null, + "scale": { + "scheme": "tableau10", + "domain": [ + "Housing", + "Transportation", + "Food", + "Healthcare", + "Everything else" + ] + } + } + }, + "config": { + "view": { + "continuousWidth": 340, + "continuousHeight": 230 + }, + "axisX": { + "labelLimit": 100, + "labelFontSize": 10, + "titleFontSize": 11 + }, + "axisY": { + "labelFontSize": 10, + "titleFontSize": 11 + }, + "legend": { + "labelFontSize": 10, + "titleFontSize": 11 + }, + "facet": { + "spacing": 23 + } + }, + "width": { + "step": 23 + }, + "data": { + "values": [ + { + "Quintile": "Lowest fifth", + "Category": "Housing", + "Spending ($)": 12800 + }, + { + "Quintile": "Lowest fifth", + "Category": "Transportation", + "Spending ($)": 4900 + }, + { + "Quintile": "Lowest fifth", + "Category": "Food", + "Spending ($)": 5100 + }, + { + "Quintile": "Lowest fifth", + "Category": "Healthcare", + "Spending ($)": 2900 + }, + { + "Quintile": "Lowest fifth", + "Category": "Everything else", + "Spending ($)": 6400 + }, + { + "Quintile": "Second fifth", + "Category": "Housing", + "Spending ($)": 16100 + }, + { + "Quintile": "Second fifth", + "Category": "Transportation", + "Spending ($)": 7600 + }, + { + "Quintile": "Second fifth", + "Category": "Food", + "Spending ($)": 6300 + }, + { + "Quintile": "Second fifth", + "Category": "Healthcare", + "Spending ($)": 4000 + }, + { + "Quintile": "Second fifth", + "Category": "Everything else", + "Spending ($)": 10700 + }, + { + "Quintile": "Middle fifth", + "Category": "Housing", + "Spending ($)": 19400 + }, + { + "Quintile": "Middle fifth", + "Category": "Transportation", + "Spending ($)": 10600 + }, + { + "Quintile": "Middle fifth", + "Category": "Food", + "Spending ($)": 7600 + }, + { + "Quintile": "Middle fifth", + "Category": "Healthcare", + "Spending ($)": 4700 + }, + { + "Quintile": "Middle fifth", + "Category": "Everything else", + "Spending ($)": 16400 + }, + { + "Quintile": "Fourth fifth", + "Category": "Housing", + "Spending ($)": 24300 + }, + { + "Quintile": "Fourth fifth", + "Category": "Transportation", + "Spending ($)": 13700 + }, + { + "Quintile": "Fourth fifth", + "Category": "Food", + "Spending ($)": 9100 + }, + { + "Quintile": "Fourth fifth", + "Category": "Healthcare", + "Spending ($)": 6100 + }, + { + "Quintile": "Fourth fifth", + "Category": "Everything else", + "Spending ($)": 22800 + }, + { + "Quintile": "Highest fifth", + "Category": "Housing", + "Spending ($)": 36600 + }, + { + "Quintile": "Highest fifth", + "Category": "Transportation", + "Spending ($)": 19500 + }, + { + "Quintile": "Highest fifth", + "Category": "Food", + "Spending ($)": 13400 + }, + { + "Quintile": "Highest fifth", + "Category": "Healthcare", + "Spending ($)": 8500 + }, + { + "Quintile": "Highest fifth", + "Category": "Everything else", + "Spending ($)": 43800 + } + ] + }, + "title": { + "text": "Where each income group's money goes", + "subtitle": [ + "Share of annual household spending, by income quintile" + ] + } +} diff --git a/site/src/playground/theme-lab-assets/spending-quintile.mckinsey.json b/site/src/playground/theme-lab-assets/spending-quintile.mckinsey.json new file mode 100644 index 00000000..ccf48cbd --- /dev/null +++ b/site/src/playground/theme-lab-assets/spending-quintile.mckinsey.json @@ -0,0 +1,144 @@ +{ + "$schema": "https://vega-lite.github.io/schema/vega-lite/v5.json", + "__theme__": "mckinsey", + "__design__": [ + "Five competing hues collapsed to one ordered blue value ramp. On a composition chart the reader tracks one band at a time, and five saturated categorical colours make all five shout at once; a ramp that darkens toward the baseline lets the eye follow the stack in the order it is built.", + "No band is singled out. The headline asks where each group's money goes, not whether housing costs too much, so promoting one category to brand blue would be the chart arguing something the data was not selected to argue.", + "Stack order pinned Housing → Transportation → Food → Healthcare → Everything else, so the largest band sits on the baseline where its height can be read against a straight edge rather than a ragged one.", + "Y axis reduced to five percent ticks with no gridlines; on a 100% stack the total is fixed, so the only rules that matter are the band boundaries.", + "Quintile labels shortened to rank names and kept horizontal — 'fifth' is already stated once in the subtitle, and a rotated category label on a five-bar chart is a layout failure, not a style choice.", + "Legend moved to the top as one horizontal row in stack order, so the key reads bottom-band-first exactly as the chart is built.", + "Bar width raised to 0.7 of the band and a 0.6px white segment stroke added, which is what separates the bands of a single-hue ramp without adding another colour." + ], + "background": "#ffffff", + "padding": { "left": 8, "top": 6, "right": 10, "bottom": 8 }, + "height": 210, + "title": { + "text": "Where each income group's money goes", + "subtitle": ["Share of annual household spending, by income quintile"] + }, + "width": { "step": 54 }, + "data": { + "values": [ + { "Quintile": "Lowest fifth", "Category": "Housing", "Spending ($)": 12800 }, + { "Quintile": "Lowest fifth", "Category": "Transportation", "Spending ($)": 4900 }, + { "Quintile": "Lowest fifth", "Category": "Food", "Spending ($)": 5100 }, + { "Quintile": "Lowest fifth", "Category": "Healthcare", "Spending ($)": 2900 }, + { "Quintile": "Lowest fifth", "Category": "Everything else", "Spending ($)": 6400 }, + { "Quintile": "Second fifth", "Category": "Housing", "Spending ($)": 16100 }, + { "Quintile": "Second fifth", "Category": "Transportation", "Spending ($)": 7600 }, + { "Quintile": "Second fifth", "Category": "Food", "Spending ($)": 6300 }, + { "Quintile": "Second fifth", "Category": "Healthcare", "Spending ($)": 4000 }, + { "Quintile": "Second fifth", "Category": "Everything else", "Spending ($)": 10700 }, + { "Quintile": "Middle fifth", "Category": "Housing", "Spending ($)": 19400 }, + { "Quintile": "Middle fifth", "Category": "Transportation", "Spending ($)": 10600 }, + { "Quintile": "Middle fifth", "Category": "Food", "Spending ($)": 7600 }, + { "Quintile": "Middle fifth", "Category": "Healthcare", "Spending ($)": 4700 }, + { "Quintile": "Middle fifth", "Category": "Everything else", "Spending ($)": 16400 }, + { "Quintile": "Fourth fifth", "Category": "Housing", "Spending ($)": 24300 }, + { "Quintile": "Fourth fifth", "Category": "Transportation", "Spending ($)": 13700 }, + { "Quintile": "Fourth fifth", "Category": "Food", "Spending ($)": 9100 }, + { "Quintile": "Fourth fifth", "Category": "Healthcare", "Spending ($)": 6100 }, + { "Quintile": "Fourth fifth", "Category": "Everything else", "Spending ($)": 22800 }, + { "Quintile": "Highest fifth", "Category": "Housing", "Spending ($)": 36600 }, + { "Quintile": "Highest fifth", "Category": "Transportation", "Spending ($)": 19500 }, + { "Quintile": "Highest fifth", "Category": "Food", "Spending ($)": 13400 }, + { "Quintile": "Highest fifth", "Category": "Healthcare", "Spending ($)": 8500 }, + { "Quintile": "Highest fifth", "Category": "Everything else", "Spending ($)": 43800 } + ] + }, + "transform": [ + { + "calculate": "indexof(['Housing', 'Transportation', 'Food', 'Healthcare', 'Everything else'], datum.Category)", + "as": "__stackRank" + } + ], + "encoding": { + "x": { + "field": "Quintile", + "type": "nominal", + "sort": ["Lowest fifth", "Second fifth", "Middle fifth", "Fourth fifth", "Highest fifth"], + "title": null, + "axis": { + "labelAngle": 0, + "labelExpr": "replace(datum.label, ' fifth', '')", + "domain": false, + "ticks": false, + "grid": false, + "labelPadding": 6 + } + }, + "y": { + "field": "Spending ($)", + "type": "quantitative", + "stack": "normalize", + "title": null, + "axis": { + "format": ".0%", + "tickCount": 5, + "grid": false, + "domain": false, + "ticks": false, + "labelPadding": 5 + } + }, + "color": { + "field": "Category", + "type": "nominal", + "title": null, + "sort": ["Housing", "Transportation", "Food", "Healthcare", "Everything else"], + "scale": { + "domain": ["Housing", "Transportation", "Food", "Healthcare", "Everything else"], + "range": ["#051c2c", "#20618f", "#4a90c4", "#9dc3e0", "#d6e4f0"] + }, + "legend": { + "orient": "top", + "direction": "horizontal", + "symbolType": "square", + "symbolSize": 100, + "columnPadding": 8, + "offset": 10, + "padding": 0 + } + }, + "order": { + "field": "__stackRank", + "type": "quantitative", + "sort": "ascending" + } + }, + "mark": { + "type": "bar", + "width": { "band": 0.7 }, + "stroke": "#ffffff", + "strokeWidth": 0.6 + }, + "config": { + "background": "#ffffff", + "font": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "title": { + "font": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "fontSize": 13, + "fontWeight": 700, + "color": "#051c2c", + "anchor": "start", + "offset": 12, + "subtitleFont": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "subtitleFontSize": 10, + "subtitleColor": "#8fa0ab", + "subtitlePadding": 7 + }, + "view": { "stroke": null }, + "axis": { + "labelFont": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "labelFontSize": 10, + "labelColor": "#051c2c" + }, + "legend": { + "labelFont": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "labelFontSize": 9.5, + "labelColor": "#051c2c", + "symbolStrokeWidth": 0 + } + } +} diff --git a/site/src/playground/theme-lab-assets/state-jobless.economist.json b/site/src/playground/theme-lab-assets/state-jobless.economist.json new file mode 100644 index 00000000..e13eb734 --- /dev/null +++ b/site/src/playground/theme-lab-assets/state-jobless.economist.json @@ -0,0 +1,148 @@ +{ + "$schema": "https://vega-lite.github.io/schema/vega-lite/v5.json", + "__theme__": "economist", + "__design__": [ + "State names turned 90° and given no label limit. This is the whole problem with fifty categories on one axis: Flint's default clips nineteen of them, and four of those clip into pairs that cannot be told apart — 'North C…' against 'North D…', 'South C…' against 'South D…'. A truncated label that is still unique costs the reader a guess; a truncated label that is not unique is simply wrong, and the chart has no way to say which Carolina it means.", + "Order left exactly as the baseline has it. Sorting these bars by rate would make a far better chart, and that is precisely why it does not belong here — what to sort by is a statement about the data, decided upstream of any house style. A design language governs how a chart looks, not what it says.", + "Value axis moved to the right-hand edge and labelled in per cent, house style — at fifty bars the scale is a reference the reader returns to, not an introduction, so it sits where the eye ends up rather than in front of the tallest bar.", + "Both axis titles dropped. 'State' sits under fifty state names, and 'Unemployment (%)' repeats a subtitle that already says 'unemployment rate … per cent'. Neither adds a word the reader does not already have.", + "Frame and vertical gridlines removed, horizontals kept in pale blue-grey behind the bars. At this density every rule that is not carrying a value is competing with a bar for the same few pixels.", + "House blue #006BA2, red masthead rule, 9.5pt labels dropping to 8pt on the category axis — sized for a print column." + ], + "background": "#ffffff", + "padding": { "left": 6, "top": 4, "right": 12, "bottom": 6 }, + "title": { + "text": "Unemployment by state", + "subtitle": ["Annual average unemployment rate, 50 US states, 2023, per cent"] + }, + "spacing": 8, + "vconcat": [ + { + "mark": { "type": "rect", "fill": "#e3120b", "stroke": null }, + "width": 26, + "height": 3, + "view": { "stroke": null } + }, + { + "width": 430, + "height": 175, + "data": { + "values": [ + { "State": "Alabama", "Unemployment (%)": 2.3 }, + { "State": "Alaska", "Unemployment (%)": 4.3 }, + { "State": "Arizona", "Unemployment (%)": 3.9 }, + { "State": "Arkansas", "Unemployment (%)": 3.3 }, + { "State": "California", "Unemployment (%)": 4.8 }, + { "State": "Colorado", "Unemployment (%)": 3.2 }, + { "State": "Connecticut", "Unemployment (%)": 3.9 }, + { "State": "Delaware", "Unemployment (%)": 4.2 }, + { "State": "Florida", "Unemployment (%)": 2.9 }, + { "State": "Georgia", "Unemployment (%)": 3.2 }, + { "State": "Hawaii", "Unemployment (%)": 3 }, + { "State": "Idaho", "Unemployment (%)": 3.2 }, + { "State": "Illinois", "Unemployment (%)": 4.5 }, + { "State": "Indiana", "Unemployment (%)": 3.3 }, + { "State": "Iowa", "Unemployment (%)": 2.9 }, + { "State": "Kansas", "Unemployment (%)": 2.8 }, + { "State": "Kentucky", "Unemployment (%)": 4.2 }, + { "State": "Louisiana", "Unemployment (%)": 3.5 }, + { "State": "Maine", "Unemployment (%)": 2.9 }, + { "State": "Maryland", "Unemployment (%)": 2.1 }, + { "State": "Massachusetts", "Unemployment (%)": 3.3 }, + { "State": "Michigan", "Unemployment (%)": 3.9 }, + { "State": "Minnesota", "Unemployment (%)": 2.8 }, + { "State": "Mississippi", "Unemployment (%)": 3.2 }, + { "State": "Missouri", "Unemployment (%)": 3 }, + { "State": "Montana", "Unemployment (%)": 2.9 }, + { "State": "Nebraska", "Unemployment (%)": 2.4 }, + { "State": "Nevada", "Unemployment (%)": 5.3 }, + { "State": "New Hampshire", "Unemployment (%)": 2.4 }, + { "State": "New Jersey", "Unemployment (%)": 4.1 }, + { "State": "New Mexico", "Unemployment (%)": 3.8 }, + { "State": "New York", "Unemployment (%)": 4.1 }, + { "State": "North Carolina", "Unemployment (%)": 3.4 }, + { "State": "North Dakota", "Unemployment (%)": 1.9 }, + { "State": "Ohio", "Unemployment (%)": 3.6 }, + { "State": "Oklahoma", "Unemployment (%)": 3 }, + { "State": "Oregon", "Unemployment (%)": 3.7 }, + { "State": "Pennsylvania", "Unemployment (%)": 3.4 }, + { "State": "Rhode Island", "Unemployment (%)": 2.9 }, + { "State": "South Carolina", "Unemployment (%)": 3 }, + { "State": "South Dakota", "Unemployment (%)": 1.9 }, + { "State": "Tennessee", "Unemployment (%)": 3.2 }, + { "State": "Texas", "Unemployment (%)": 4 }, + { "State": "Utah", "Unemployment (%)": 2.5 }, + { "State": "Vermont", "Unemployment (%)": 2.1 }, + { "State": "Virginia", "Unemployment (%)": 2.8 }, + { "State": "Washington", "Unemployment (%)": 4.1 }, + { "State": "West Virginia", "Unemployment (%)": 4 }, + { "State": "Wisconsin", "Unemployment (%)": 2.9 }, + { "State": "Wyoming", "Unemployment (%)": 3.1 } + ] + }, + "encoding": { + "x": { + "field": "State", + "type": "nominal", + "title": null, + "sort": null, + "scale": { "paddingInner": 0.25 }, + "axis": { + "labelAngle": -90, + "labelAlign": "right", + "labelBaseline": "middle", + "labelFontSize": 8, + "labelLimit": 0, + "labelPadding": 4, + "domain": true, + "domainColor": "#121317", + "domainWidth": 1, + "grid": false + } + }, + "y": { + "field": "Unemployment (%)", + "type": "quantitative", + "title": null, + "scale": { "zero": true, "nice": false, "domain": [0, 5.5] }, + "axis": { + "orient": "right", + "values": [0, 1, 2, 3, 4, 5], + "labelExpr": "datum.value + '%'", + "grid": true, + "gridColor": "#c9d3da", + "domain": false, + "ticks": false, + "labelPadding": 4 + } + } + }, + "mark": { "type": "bar", "color": "#006ba2" } + } + ], + "config": { + "background": "#ffffff", + "font": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "title": { + "font": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "fontSize": 13.5, + "fontWeight": 700, + "color": "#121317", + "anchor": "start", + "offset": 10, + "subtitleFont": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "subtitleFontSize": 10.5, + "subtitleColor": "#54585a", + "subtitlePadding": 6 + }, + "view": { "stroke": null }, + "axis": { + "labelFont": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "labelFontSize": 9.5, + "labelColor": "#54585a", + "grid": false, + "domain": false, + "ticks": false + } + } +} diff --git a/site/src/playground/theme-lab-assets/state-jobless.flint.json b/site/src/playground/theme-lab-assets/state-jobless.flint.json new file mode 100644 index 00000000..0cb5919d --- /dev/null +++ b/site/src/playground/theme-lab-assets/state-jobless.flint.json @@ -0,0 +1,258 @@ +{ + "mark": "bar", + "encoding": { + "x": { + "field": "State", + "type": "nominal", + "sort": null + }, + "y": { + "field": "Unemployment (%)", + "type": "quantitative", + "scale": { + "zero": true + }, + "axis": { + "format": ",.12~g" + } + } + }, + "config": { + "view": { + "continuousWidth": 340, + "continuousHeight": 144 + }, + "axisX": { + "labelLimit": 40, + "labelFontSize": 6, + "titleFontSize": 9, + "labelAngle": -90, + "labelAlign": "right", + "labelBaseline": "middle" + }, + "axisY": { + "labelFontSize": 6, + "titleFontSize": 9 + }, + "legend": { + "labelFontSize": 9, + "titleFontSize": 9 + }, + "facet": { + "spacing": 14 + } + }, + "width": { + "step": 9 + }, + "data": { + "values": [ + { + "State": "Alabama", + "Unemployment (%)": 2.3 + }, + { + "State": "Alaska", + "Unemployment (%)": 4.3 + }, + { + "State": "Arizona", + "Unemployment (%)": 3.9 + }, + { + "State": "Arkansas", + "Unemployment (%)": 3.3 + }, + { + "State": "California", + "Unemployment (%)": 4.8 + }, + { + "State": "Colorado", + "Unemployment (%)": 3.2 + }, + { + "State": "Connecticut", + "Unemployment (%)": 3.9 + }, + { + "State": "Delaware", + "Unemployment (%)": 4.2 + }, + { + "State": "Florida", + "Unemployment (%)": 2.9 + }, + { + "State": "Georgia", + "Unemployment (%)": 3.2 + }, + { + "State": "Hawaii", + "Unemployment (%)": 3 + }, + { + "State": "Idaho", + "Unemployment (%)": 3.2 + }, + { + "State": "Illinois", + "Unemployment (%)": 4.5 + }, + { + "State": "Indiana", + "Unemployment (%)": 3.3 + }, + { + "State": "Iowa", + "Unemployment (%)": 2.9 + }, + { + "State": "Kansas", + "Unemployment (%)": 2.8 + }, + { + "State": "Kentucky", + "Unemployment (%)": 4.2 + }, + { + "State": "Louisiana", + "Unemployment (%)": 3.5 + }, + { + "State": "Maine", + "Unemployment (%)": 2.9 + }, + { + "State": "Maryland", + "Unemployment (%)": 2.1 + }, + { + "State": "Massachusetts", + "Unemployment (%)": 3.3 + }, + { + "State": "Michigan", + "Unemployment (%)": 3.9 + }, + { + "State": "Minnesota", + "Unemployment (%)": 2.8 + }, + { + "State": "Mississippi", + "Unemployment (%)": 3.2 + }, + { + "State": "Missouri", + "Unemployment (%)": 3 + }, + { + "State": "Montana", + "Unemployment (%)": 2.9 + }, + { + "State": "Nebraska", + "Unemployment (%)": 2.4 + }, + { + "State": "Nevada", + "Unemployment (%)": 5.3 + }, + { + "State": "New Hampshire", + "Unemployment (%)": 2.4 + }, + { + "State": "New Jersey", + "Unemployment (%)": 4.1 + }, + { + "State": "New Mexico", + "Unemployment (%)": 3.8 + }, + { + "State": "New York", + "Unemployment (%)": 4.1 + }, + { + "State": "North Carolina", + "Unemployment (%)": 3.4 + }, + { + "State": "North Dakota", + "Unemployment (%)": 1.9 + }, + { + "State": "Ohio", + "Unemployment (%)": 3.6 + }, + { + "State": "Oklahoma", + "Unemployment (%)": 3 + }, + { + "State": "Oregon", + "Unemployment (%)": 3.7 + }, + { + "State": "Pennsylvania", + "Unemployment (%)": 3.4 + }, + { + "State": "Rhode Island", + "Unemployment (%)": 2.9 + }, + { + "State": "South Carolina", + "Unemployment (%)": 3 + }, + { + "State": "South Dakota", + "Unemployment (%)": 1.9 + }, + { + "State": "Tennessee", + "Unemployment (%)": 3.2 + }, + { + "State": "Texas", + "Unemployment (%)": 4 + }, + { + "State": "Utah", + "Unemployment (%)": 2.5 + }, + { + "State": "Vermont", + "Unemployment (%)": 2.1 + }, + { + "State": "Virginia", + "Unemployment (%)": 2.8 + }, + { + "State": "Washington", + "Unemployment (%)": 4.1 + }, + { + "State": "West Virginia", + "Unemployment (%)": 4 + }, + { + "State": "Wisconsin", + "Unemployment (%)": 2.9 + }, + { + "State": "Wyoming", + "Unemployment (%)": 3.1 + } + ] + }, + "title": { + "text": "Unemployment by state", + "subtitle": [ + "Annual average unemployment rate, 50 US states, 2023, per cent" + ] + } +} diff --git a/site/src/playground/theme-lab-assets/state-unemployment.datawrapper.json b/site/src/playground/theme-lab-assets/state-unemployment.datawrapper.json new file mode 100644 index 00000000..234cdb27 --- /dev/null +++ b/site/src/playground/theme-lab-assets/state-unemployment.datawrapper.json @@ -0,0 +1,380 @@ +{ + "$schema": "https://vega-lite.github.io/schema/vega-lite/v5.json", + "__theme__": "datawrapper", + "__design__": [ + "Continuous 'goldgreen' ramp replaced with a five-step quantize scale on a single blue hue. A continuous gradient across fifty small polygons is unreadable — the eye cannot invert a smooth ramp, so the values get binned into classes the reader can actually name.", + "Hue reduced to one family (#DCEEF6 → #0B5C82). Two-hue schemes on a choropleth imply a diverging quantity; unemployment has no meaningful midpoint here, so it gets a sequential ramp.", + "Legend moved from a right-hand gradient bar to a horizontal row of discrete swatches above the map, so the map itself keeps the full width of the tile.", + "Legend labels formatted with a % suffix and the class breaks stated explicitly (2, 3, 4, 5) rather than implied by tick positions on a gradient.", + "State boundaries thinned to 0.6px white and a 0.8px white outer edge added, which is what stops dense north-eastern states from merging into one blob.", + "Frame, view stroke and all padding removed and the projection given the whole 460×280 box; a map has no axes, so every pixel of chrome was waste.", + "Tooltip fields kept but the value formatted to one decimal with a unit, matching the legend so hover and key agree.", + "Key breaks relabelled to one decimal with overlap removal switched off. At .0f the five bands read 2%, 3%, 3%, 4%, 4% and Vega then dropped the collisions, so three of the five swatches had no readable break at all." + ], + "background": "#ffffff", + "padding": { + "left": 0, + "top": 4, + "right": 0, + "bottom": 4 + }, + "title": { + "text": "Unemployment rate by state, 2023", + "subtitle": [ + "Annual average, % of the civilian labour force" + ] + }, + "width": 460, + "height": 280, + "data": { + "url": "https://vega.github.io/vega-lite/data/us-10m.json", + "format": { + "type": "topojson", + "feature": "states" + } + }, + "projection": { + "type": "albersUsa" + }, + "transform": [ + { + "lookup": "id", + "from": { + "data": { + "values": [ + { + "State": "Alabama", + "Unemployment (%)": 2.3, + "__geo_id": 1 + }, + { + "State": "Alaska", + "Unemployment (%)": 4.3, + "__geo_id": 2 + }, + { + "State": "Arizona", + "Unemployment (%)": 3.9, + "__geo_id": 4 + }, + { + "State": "Arkansas", + "Unemployment (%)": 3.3, + "__geo_id": 5 + }, + { + "State": "California", + "Unemployment (%)": 4.8, + "__geo_id": 6 + }, + { + "State": "Colorado", + "Unemployment (%)": 3.2, + "__geo_id": 8 + }, + { + "State": "Connecticut", + "Unemployment (%)": 3.9, + "__geo_id": 9 + }, + { + "State": "Delaware", + "Unemployment (%)": 4.2, + "__geo_id": 10 + }, + { + "State": "Florida", + "Unemployment (%)": 2.9, + "__geo_id": 12 + }, + { + "State": "Georgia", + "Unemployment (%)": 3.2, + "__geo_id": 13 + }, + { + "State": "Hawaii", + "Unemployment (%)": 3, + "__geo_id": 15 + }, + { + "State": "Idaho", + "Unemployment (%)": 3.2, + "__geo_id": 16 + }, + { + "State": "Illinois", + "Unemployment (%)": 4.5, + "__geo_id": 17 + }, + { + "State": "Indiana", + "Unemployment (%)": 3.3, + "__geo_id": 18 + }, + { + "State": "Iowa", + "Unemployment (%)": 2.9, + "__geo_id": 19 + }, + { + "State": "Kansas", + "Unemployment (%)": 2.8, + "__geo_id": 20 + }, + { + "State": "Kentucky", + "Unemployment (%)": 4.2, + "__geo_id": 21 + }, + { + "State": "Louisiana", + "Unemployment (%)": 3.5, + "__geo_id": 22 + }, + { + "State": "Maine", + "Unemployment (%)": 2.9, + "__geo_id": 23 + }, + { + "State": "Maryland", + "Unemployment (%)": 2.1, + "__geo_id": 24 + }, + { + "State": "Massachusetts", + "Unemployment (%)": 3.3, + "__geo_id": 25 + }, + { + "State": "Michigan", + "Unemployment (%)": 3.9, + "__geo_id": 26 + }, + { + "State": "Minnesota", + "Unemployment (%)": 2.8, + "__geo_id": 27 + }, + { + "State": "Mississippi", + "Unemployment (%)": 3.2, + "__geo_id": 28 + }, + { + "State": "Missouri", + "Unemployment (%)": 3, + "__geo_id": 29 + }, + { + "State": "Montana", + "Unemployment (%)": 2.9, + "__geo_id": 30 + }, + { + "State": "Nebraska", + "Unemployment (%)": 2.4, + "__geo_id": 31 + }, + { + "State": "Nevada", + "Unemployment (%)": 5.3, + "__geo_id": 32 + }, + { + "State": "New Hampshire", + "Unemployment (%)": 2.4, + "__geo_id": 33 + }, + { + "State": "New Jersey", + "Unemployment (%)": 4.1, + "__geo_id": 34 + }, + { + "State": "New Mexico", + "Unemployment (%)": 3.8, + "__geo_id": 35 + }, + { + "State": "New York", + "Unemployment (%)": 4.1, + "__geo_id": 36 + }, + { + "State": "North Carolina", + "Unemployment (%)": 3.4, + "__geo_id": 37 + }, + { + "State": "North Dakota", + "Unemployment (%)": 1.9, + "__geo_id": 38 + }, + { + "State": "Ohio", + "Unemployment (%)": 3.6, + "__geo_id": 39 + }, + { + "State": "Oklahoma", + "Unemployment (%)": 3, + "__geo_id": 40 + }, + { + "State": "Oregon", + "Unemployment (%)": 3.7, + "__geo_id": 41 + }, + { + "State": "Pennsylvania", + "Unemployment (%)": 3.4, + "__geo_id": 42 + }, + { + "State": "Rhode Island", + "Unemployment (%)": 2.9, + "__geo_id": 44 + }, + { + "State": "South Carolina", + "Unemployment (%)": 3, + "__geo_id": 45 + }, + { + "State": "South Dakota", + "Unemployment (%)": 1.9, + "__geo_id": 46 + }, + { + "State": "Tennessee", + "Unemployment (%)": 3.2, + "__geo_id": 47 + }, + { + "State": "Texas", + "Unemployment (%)": 4, + "__geo_id": 48 + }, + { + "State": "Utah", + "Unemployment (%)": 2.5, + "__geo_id": 49 + }, + { + "State": "Vermont", + "Unemployment (%)": 2.1, + "__geo_id": 50 + }, + { + "State": "Virginia", + "Unemployment (%)": 2.8, + "__geo_id": 51 + }, + { + "State": "Washington", + "Unemployment (%)": 4.1, + "__geo_id": 53 + }, + { + "State": "West Virginia", + "Unemployment (%)": 4, + "__geo_id": 54 + }, + { + "State": "Wisconsin", + "Unemployment (%)": 2.9, + "__geo_id": 55 + }, + { + "State": "Wyoming", + "Unemployment (%)": 3.1, + "__geo_id": 56 + } + ] + }, + "key": "__geo_id", + "fields": [ + "Unemployment (%)", + "State" + ] + } + } + ], + "mark": { + "type": "geoshape", + "stroke": "#ffffff", + "strokeWidth": 0.6 + }, + "encoding": { + "color": { + "field": "Unemployment (%)", + "type": "quantitative", + "title": null, + "scale": { + "type": "quantize", + "domain": [ + 2, + 5 + ], + "range": [ + "#dceef6", + "#a9d3e6", + "#6aabcc", + "#2f7fa8", + "#0b5c82" + ] + }, + "legend": { + "orient": "top", + "direction": "horizontal", + "type": "symbol", + "symbolType": "square", + "symbolSize": 130, + "format": ".1f", + "labelExpr": "isValid(datum.label) ? datum.label + '%' : ''", + "columnPadding": 6, + "offset": 2, + "padding": 0, + "labelOverlap": false + } + }, + "tooltip": [ + { + "field": "State", + "type": "nominal" + }, + { + "field": "Unemployment (%)", + "type": "quantitative", + "format": ".1f" + } + ] + }, + "config": { + "background": "#ffffff", + "font": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "title": { + "font": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "fontSize": 15, + "fontWeight": 700, + "color": "#333333", + "anchor": "start", + "offset": 12, + "subtitleFont": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "subtitleFontSize": 11, + "subtitleColor": "#767676", + "subtitlePadding": 8 + }, + "view": { + "stroke": null + }, + "legend": { + "labelFont": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "labelFontSize": 10.5, + "labelColor": "#333333", + "symbolStrokeWidth": 0 + } + } +} diff --git a/site/src/playground/theme-lab-assets/state-unemployment.flint.json b/site/src/playground/theme-lab-assets/state-unemployment.flint.json new file mode 100644 index 00000000..2930bd7f --- /dev/null +++ b/site/src/playground/theme-lab-assets/state-unemployment.flint.json @@ -0,0 +1,332 @@ +{ + "mark": { + "type": "geoshape", + "stroke": "white", + "strokeWidth": 0.5 + }, + "encoding": { + "color": { + "field": "Unemployment (%)", + "type": "quantitative", + "scale": { + "scheme": "goldgreen" + } + }, + "tooltip": [ + { + "field": "State", + "type": "nominal" + }, + { + "field": "Unemployment (%)", + "type": "quantitative" + } + ] + }, + "width": 500, + "height": 300, + "data": { + "url": "https://vega.github.io/vega-lite/data/us-10m.json", + "format": { + "type": "topojson", + "feature": "states" + } + }, + "projection": { + "type": "albersUsa" + }, + "transform": [ + { + "lookup": "id", + "from": { + "data": { + "values": [ + { + "State": "Alabama", + "Unemployment (%)": 2.3, + "__geo_id": 1 + }, + { + "State": "Alaska", + "Unemployment (%)": 4.3, + "__geo_id": 2 + }, + { + "State": "Arizona", + "Unemployment (%)": 3.9, + "__geo_id": 4 + }, + { + "State": "Arkansas", + "Unemployment (%)": 3.3, + "__geo_id": 5 + }, + { + "State": "California", + "Unemployment (%)": 4.8, + "__geo_id": 6 + }, + { + "State": "Colorado", + "Unemployment (%)": 3.2, + "__geo_id": 8 + }, + { + "State": "Connecticut", + "Unemployment (%)": 3.9, + "__geo_id": 9 + }, + { + "State": "Delaware", + "Unemployment (%)": 4.2, + "__geo_id": 10 + }, + { + "State": "Florida", + "Unemployment (%)": 2.9, + "__geo_id": 12 + }, + { + "State": "Georgia", + "Unemployment (%)": 3.2, + "__geo_id": 13 + }, + { + "State": "Hawaii", + "Unemployment (%)": 3, + "__geo_id": 15 + }, + { + "State": "Idaho", + "Unemployment (%)": 3.2, + "__geo_id": 16 + }, + { + "State": "Illinois", + "Unemployment (%)": 4.5, + "__geo_id": 17 + }, + { + "State": "Indiana", + "Unemployment (%)": 3.3, + "__geo_id": 18 + }, + { + "State": "Iowa", + "Unemployment (%)": 2.9, + "__geo_id": 19 + }, + { + "State": "Kansas", + "Unemployment (%)": 2.8, + "__geo_id": 20 + }, + { + "State": "Kentucky", + "Unemployment (%)": 4.2, + "__geo_id": 21 + }, + { + "State": "Louisiana", + "Unemployment (%)": 3.5, + "__geo_id": 22 + }, + { + "State": "Maine", + "Unemployment (%)": 2.9, + "__geo_id": 23 + }, + { + "State": "Maryland", + "Unemployment (%)": 2.1, + "__geo_id": 24 + }, + { + "State": "Massachusetts", + "Unemployment (%)": 3.3, + "__geo_id": 25 + }, + { + "State": "Michigan", + "Unemployment (%)": 3.9, + "__geo_id": 26 + }, + { + "State": "Minnesota", + "Unemployment (%)": 2.8, + "__geo_id": 27 + }, + { + "State": "Mississippi", + "Unemployment (%)": 3.2, + "__geo_id": 28 + }, + { + "State": "Missouri", + "Unemployment (%)": 3, + "__geo_id": 29 + }, + { + "State": "Montana", + "Unemployment (%)": 2.9, + "__geo_id": 30 + }, + { + "State": "Nebraska", + "Unemployment (%)": 2.4, + "__geo_id": 31 + }, + { + "State": "Nevada", + "Unemployment (%)": 5.3, + "__geo_id": 32 + }, + { + "State": "New Hampshire", + "Unemployment (%)": 2.4, + "__geo_id": 33 + }, + { + "State": "New Jersey", + "Unemployment (%)": 4.1, + "__geo_id": 34 + }, + { + "State": "New Mexico", + "Unemployment (%)": 3.8, + "__geo_id": 35 + }, + { + "State": "New York", + "Unemployment (%)": 4.1, + "__geo_id": 36 + }, + { + "State": "North Carolina", + "Unemployment (%)": 3.4, + "__geo_id": 37 + }, + { + "State": "North Dakota", + "Unemployment (%)": 1.9, + "__geo_id": 38 + }, + { + "State": "Ohio", + "Unemployment (%)": 3.6, + "__geo_id": 39 + }, + { + "State": "Oklahoma", + "Unemployment (%)": 3, + "__geo_id": 40 + }, + { + "State": "Oregon", + "Unemployment (%)": 3.7, + "__geo_id": 41 + }, + { + "State": "Pennsylvania", + "Unemployment (%)": 3.4, + "__geo_id": 42 + }, + { + "State": "Rhode Island", + "Unemployment (%)": 2.9, + "__geo_id": 44 + }, + { + "State": "South Carolina", + "Unemployment (%)": 3, + "__geo_id": 45 + }, + { + "State": "South Dakota", + "Unemployment (%)": 1.9, + "__geo_id": 46 + }, + { + "State": "Tennessee", + "Unemployment (%)": 3.2, + "__geo_id": 47 + }, + { + "State": "Texas", + "Unemployment (%)": 4, + "__geo_id": 48 + }, + { + "State": "Utah", + "Unemployment (%)": 2.5, + "__geo_id": 49 + }, + { + "State": "Vermont", + "Unemployment (%)": 2.1, + "__geo_id": 50 + }, + { + "State": "Virginia", + "Unemployment (%)": 2.8, + "__geo_id": 51 + }, + { + "State": "Washington", + "Unemployment (%)": 4.1, + "__geo_id": 53 + }, + { + "State": "West Virginia", + "Unemployment (%)": 4, + "__geo_id": 54 + }, + { + "State": "Wisconsin", + "Unemployment (%)": 2.9, + "__geo_id": 55 + }, + { + "State": "Wyoming", + "Unemployment (%)": 3.1, + "__geo_id": 56 + } + ] + }, + "key": "__geo_id", + "fields": [ + "Unemployment (%)", + "State" + ] + } + } + ], + "config": { + "view": { + "continuousWidth": 500, + "continuousHeight": 300 + }, + "axisX": { + "labelLimit": 100, + "labelFontSize": 10, + "titleFontSize": 11 + }, + "axisY": { + "labelFontSize": 10, + "titleFontSize": 11 + }, + "legend": { + "labelFontSize": 10, + "titleFontSize": 11 + }, + "facet": { + "spacing": 23 + } + }, + "title": { + "text": "Unemployment rate by state, 2023", + "subtitle": [ + "Annual average, % of the civilian labour force" + ] + } +} diff --git a/site/src/playground/theme-lab-assets/stock-candle.flint.json b/site/src/playground/theme-lab-assets/stock-candle.flint.json new file mode 100644 index 00000000..df6de1cc --- /dev/null +++ b/site/src/playground/theme-lab-assets/stock-candle.flint.json @@ -0,0 +1,171 @@ +{ + "encoding": { + "x": { + "field": "Date", + "type": "temporal", + "scale": { + "nice": false, + "domain": [ + "2024-01-01T09:00:00.000Z", + "2024-01-12T15:00:00.000Z" + ] + } + }, + "y": { + "type": "quantitative", + "scale": { + "zero": false + }, + "axis": { + "title": null, + "format": ",.12~g" + } + }, + "color": { + "condition": { + "test": "datum['Open'] <= datum['Close']", + "value": "#06982d" + }, + "value": "#ae1325" + } + }, + "layer": [ + { + "mark": "rule", + "encoding": { + "y": { + "field": "Low" + }, + "y2": { + "field": "High" + } + } + }, + { + "mark": { + "type": "bar", + "size": 16 + }, + "encoding": { + "y": { + "field": "Open" + }, + "y2": { + "field": "Close" + } + } + }, + { + "transform": [ + { + "filter": "datum['Open'] === datum['Close']" + } + ], + "mark": { + "type": "tick", + "size": 16, + "thickness": 2 + }, + "encoding": { + "y": { + "field": "Close" + } + } + } + ], + "title": { + "text": "Two weeks of a stock going nowhere", + "subtitle": [ + "Daily open, high, low and close, 2–12 January 2024" + ] + }, + "config": { + "view": { + "continuousWidth": 230, + "continuousHeight": 230 + }, + "axisX": { + "labelLimit": 100, + "labelFontSize": 10, + "titleFontSize": 11 + }, + "axisY": { + "labelFontSize": 10, + "titleFontSize": 11 + }, + "legend": { + "labelFontSize": 10, + "titleFontSize": 11 + }, + "facet": { + "spacing": 23 + } + }, + "data": { + "values": [ + { + "Date": "2024-01-02", + "Open": 187, + "High": 188, + "Low": 183, + "Close": 185 + }, + { + "Date": "2024-01-03", + "Open": 184, + "High": 185, + "Low": 182, + "Close": 184 + }, + { + "Date": "2024-01-04", + "Open": 182, + "High": 183, + "Low": 180, + "Close": 182 + }, + { + "Date": "2024-01-05", + "Open": 182, + "High": 182, + "Low": 179, + "Close": 181 + }, + { + "Date": "2024-01-08", + "Open": 182, + "High": 186, + "Low": 182, + "Close": 185 + }, + { + "Date": "2024-01-09", + "Open": 184, + "High": 185, + "Low": 183, + "Close": 185 + }, + { + "Date": "2024-01-10", + "Open": 184, + "High": 186, + "Low": 183, + "Close": 186 + }, + { + "Date": "2024-01-11", + "Open": 186, + "High": 187, + "Low": 183, + "Close": 186 + }, + { + "Date": "2024-01-12", + "Open": 186, + "High": 188, + "Low": 185, + "Close": 185 + } + ] + } +} diff --git a/site/src/playground/theme-lab-assets/stock-candle.powerbi.json b/site/src/playground/theme-lab-assets/stock-candle.powerbi.json new file mode 100644 index 00000000..1622703f --- /dev/null +++ b/site/src/playground/theme-lab-assets/stock-candle.powerbi.json @@ -0,0 +1,143 @@ +{ + "$schema": "https://vega-lite.github.io/schema/vega-lite/v5.json", + "__theme__": "powerbi", + "__design__": [ + "Trading days made ordinal. Flint puts the dates on a temporal scale, so the weekend between 5 and 8 January opens a two-day hole in a series that has no data on either side of it — the chart draws the absence of trading as a price gap. Nine sessions, nine equal slots, and the body width stated as a fraction of the band rather than in pixels, so it holds at any size.", + "Flint's hardcoded temporal domain — '2024-01-01T09:00:00.000Z' to '2024-01-12T15:00:00.000Z', an ISO instant with a UTC offset, on daily bars — deleted with it. It is a padding hack expressed as a timestamp, and it silently shifts every candle if the reader's data is in another zone.", + "#06982D / #AE1325 replaced by #22B14C and #E66C37. Saturated green against saturated red is the one pairing that collapses for the roughly 8% of men with a red–green deficiency, and it is exactly the pairing where direction is the entire message. Green against orange survives.", + "Flat sessions given a body rather than a hairline. On 3, 4 and 11 January the open equals the close, so the candle has no height of its own; Flint draws the convention, a 2px horizontal tick. On a dark dashboard at this size that reads as an absence, so the body is padded to ±0.09 instead — the same event, stated at the weight of its neighbours.", + "Y axis given its unit and a domain pinned to 178–190, so the two-week range is read against a stated scale rather than whatever 'zero: false' happens to produce. Flint's ',.12~g' format — twelve significant digits on a dollar price — replaced by currency.", + "Latest close labelled at the right, in the direction colour. On a dashboard the last number is the one being looked for.", + "Inverted to the #1B1A19 canvas with 9.5pt Segoe UI and dashed #3B3A39 horizontal gridlines that sit behind the candles instead of competing with them." + ], + "background": "#1b1a19", + "padding": { "left": 10, "top": 6, "right": 14, "bottom": 8 }, + "title": { + "text": "Two weeks of a stock going nowhere", + "subtitle": ["Daily open, high, low and close, 2–12 January 2024"] + }, + "width": 250, + "height": 175, + "data": { + "values": [ + { "Date": "2024-01-02", "Open": 187, "High": 188, "Low": 183, "Close": 185 }, + { "Date": "2024-01-03", "Open": 184, "High": 185, "Low": 182, "Close": 184 }, + { "Date": "2024-01-04", "Open": 182, "High": 183, "Low": 180, "Close": 182 }, + { "Date": "2024-01-05", "Open": 182, "High": 182, "Low": 179, "Close": 181 }, + { "Date": "2024-01-08", "Open": 182, "High": 186, "Low": 182, "Close": 185 }, + { "Date": "2024-01-09", "Open": 184, "High": 185, "Low": 183, "Close": 185 }, + { "Date": "2024-01-10", "Open": 184, "High": 186, "Low": 183, "Close": 186 }, + { "Date": "2024-01-11", "Open": 186, "High": 187, "Low": 183, "Close": 186 }, + { "Date": "2024-01-12", "Open": 186, "High": 188, "Low": 185, "Close": 185 } + ] + }, + "transform": [ + { + "calculate": "datum.Open === datum.Close ? datum.Open - 0.09 : min(datum.Open, datum.Close)", + "as": "__bodyLo" + }, + { + "calculate": "datum.Open === datum.Close ? datum.Open + 0.09 : max(datum.Open, datum.Close)", + "as": "__bodyHi" + } + ], + "encoding": { + "x": { + "field": "Date", + "type": "ordinal", + "title": "January 2024", + "scale": { "paddingInner": 0.4 }, + "axis": { + "labelExpr": "toNumber(slice(datum.value, 8))", + "labelAngle": 0, + "grid": false, + "domain": false, + "ticks": false, + "labelPadding": 4 + } + }, + "y": { + "type": "quantitative", + "title": "US dollars", + "scale": { "zero": false, "nice": false, "domain": [178, 190] }, + "axis": { + "values": [178, 180, 182, 184, 186, 188, 190], + "format": "$d", + "grid": true, + "gridColor": "#3b3a39", + "gridDash": [3, 3], + "domain": false, + "ticks": false, + "titleAngle": 0, + "titleAlign": "left", + "titleAnchor": "start", + "titleBaseline": "bottom", + "titleY": -8, + "titlePadding": 0 + } + }, + "color": { + "condition": { "test": "datum.Open <= datum.Close", "value": "#22b14c" }, + "value": "#e66c37" + } + }, + "layer": [ + { + "mark": { "type": "rule", "strokeWidth": 1 }, + "encoding": { + "y": { "field": "Low", "type": "quantitative" }, + "y2": { "field": "High" } + } + }, + { + "mark": { "type": "bar", "width": { "band": 0.6 } }, + "encoding": { + "y": { "field": "__bodyLo", "type": "quantitative" }, + "y2": { "field": "__bodyHi" } + } + }, + { + "transform": [{ "filter": "datum.Date === '2024-01-12'" }], + "mark": { + "type": "text", + "align": "left", + "baseline": "middle", + "dx": 10, + "fontSize": 11, + "fontWeight": 600 + }, + "encoding": { + "y": { "field": "Close", "type": "quantitative" }, + "text": { "field": "Close", "type": "quantitative", "format": "$d" } + } + } + ], + "config": { + "background": "#1b1a19", + "font": "'Segoe UI', 'Helvetica Neue', Arial, sans-serif", + "title": { + "font": "'Segoe UI', 'Helvetica Neue', Arial, sans-serif", + "fontSize": 12.5, + "fontWeight": 600, + "color": "#f3f2f1", + "anchor": "start", + "offset": 10, + "subtitleFont": "'Segoe UI', 'Helvetica Neue', Arial, sans-serif", + "subtitleFontSize": 9.5, + "subtitleColor": "#8a8886", + "subtitlePadding": 6 + }, + "view": { "stroke": null }, + "axis": { + "labelFont": "'Segoe UI', 'Helvetica Neue', Arial, sans-serif", + "labelFontSize": 9.5, + "labelColor": "#8a8886", + "titleFont": "'Segoe UI', 'Helvetica Neue', Arial, sans-serif", + "titleFontSize": 9.5, + "titleColor": "#8a8886", + "grid": false, + "domain": false, + "ticks": false + } + } +} diff --git a/site/src/playground/theme-lab-assets/sunspots.flint.json b/site/src/playground/theme-lab-assets/sunspots.flint.json new file mode 100644 index 00000000..95959a14 --- /dev/null +++ b/site/src/playground/theme-lab-assets/sunspots.flint.json @@ -0,0 +1,144 @@ +{ + "mark": "line", + "encoding": { + "x": { + "field": "Year", + "type": "temporal" + }, + "y": { + "field": "Sunspot number", + "type": "quantitative", + "scale": { + "zero": true + }, + "axis": { + "format": ",.12~g" + } + } + }, + "config": { + "view": { + "continuousWidth": 332, + "continuousHeight": 235 + }, + "axisX": { + "labelLimit": 100, + "labelFontSize": 10, + "titleFontSize": 11 + }, + "axisY": { + "labelFontSize": 10, + "titleFontSize": 11 + }, + "legend": { + "labelFontSize": 10, + "titleFontSize": 11 + }, + "facet": { + "spacing": 24 + } + }, + "data": { + "values": [ + { + "Year": "2000", + "Sunspot number": 120 + }, + { + "Year": "2001", + "Sunspot number": 111 + }, + { + "Year": "2002", + "Sunspot number": 104 + }, + { + "Year": "2003", + "Sunspot number": 64 + }, + { + "Year": "2004", + "Sunspot number": 40 + }, + { + "Year": "2005", + "Sunspot number": 30 + }, + { + "Year": "2006", + "Sunspot number": 15 + }, + { + "Year": "2007", + "Sunspot number": 8 + }, + { + "Year": "2008", + "Sunspot number": 3 + }, + { + "Year": "2009", + "Sunspot number": 3 + }, + { + "Year": "2010", + "Sunspot number": 16 + }, + { + "Year": "2011", + "Sunspot number": 56 + }, + { + "Year": "2012", + "Sunspot number": 58 + }, + { + "Year": "2013", + "Sunspot number": 65 + }, + { + "Year": "2014", + "Sunspot number": 79 + }, + { + "Year": "2015", + "Sunspot number": 67 + }, + { + "Year": "2016", + "Sunspot number": 40 + }, + { + "Year": "2017", + "Sunspot number": 22 + }, + { + "Year": "2018", + "Sunspot number": 7 + }, + { + "Year": "2019", + "Sunspot number": 4 + }, + { + "Year": "2020", + "Sunspot number": 9 + }, + { + "Year": "2021", + "Sunspot number": 29 + }, + { + "Year": "2022", + "Sunspot number": 83 + }, + { + "Year": "2023", + "Sunspot number": 123 + } + ] + }, + "title": { + "text": "Sunspot number, 2000–2023" + } +} diff --git a/site/src/playground/theme-lab-assets/temp-anomaly.flint.json b/site/src/playground/theme-lab-assets/temp-anomaly.flint.json new file mode 100644 index 00000000..9258b2fc --- /dev/null +++ b/site/src/playground/theme-lab-assets/temp-anomaly.flint.json @@ -0,0 +1,108 @@ +{ + "mark": "bar", + "encoding": { + "x": { + "field": "Decade", + "type": "nominal", + "sort": null + }, + "y": { + "field": "Anomaly (°C)", + "type": "quantitative", + "scale": { + "zero": true + }, + "axis": { + "format": ",.12~g" + } + }, + "color": { + "field": "Direction", + "type": "nominal", + "sort": null, + "scale": { + "scheme": "tableau10" + } + } + }, + "config": { + "view": { + "continuousWidth": 340, + "continuousHeight": 230 + }, + "axisX": { + "labelLimit": 100, + "labelFontSize": 10, + "titleFontSize": 11 + }, + "axisY": { + "labelFontSize": 10, + "titleFontSize": 11 + }, + "legend": { + "labelFontSize": 10, + "titleFontSize": 11 + }, + "facet": { + "spacing": 23 + } + }, + "width": { + "step": 23 + }, + "data": { + "values": [ + { + "Decade": "1880s", + "Anomaly (°C)": -0.17, + "Direction": "Below average" + }, + { + "Decade": "1900s", + "Anomaly (°C)": -0.16, + "Direction": "Below average" + }, + { + "Decade": "1920s", + "Anomaly (°C)": -0.27, + "Direction": "Below average" + }, + { + "Decade": "1940s", + "Anomaly (°C)": 0.12, + "Direction": "Above average" + }, + { + "Decade": "1960s", + "Anomaly (°C)": -0.03, + "Direction": "Below average" + }, + { + "Decade": "1980s", + "Anomaly (°C)": 0.26, + "Direction": "Above average" + }, + { + "Decade": "2000s", + "Anomaly (°C)": 0.4, + "Direction": "Above average" + }, + { + "Decade": "2010s", + "Anomaly (°C)": 0.72, + "Direction": "Above average" + }, + { + "Decade": "2020s", + "Anomaly (°C)": 1.02, + "Direction": "Above average" + } + ] + }, + "title": { + "text": "Global temperature anomaly by decade", + "subtitle": [ + "°C against the 1951–1980 average" + ] + } +} diff --git a/site/src/playground/theme-lab-assets/temp-anomaly.nyt.json b/site/src/playground/theme-lab-assets/temp-anomaly.nyt.json new file mode 100644 index 00000000..a2cde783 --- /dev/null +++ b/site/src/playground/theme-lab-assets/temp-anomaly.nyt.json @@ -0,0 +1,110 @@ +{ + "$schema": "https://vega-lite.github.io/schema/vega-lite/v5.json", + "__theme__": "nyt", + "__design__": [ + "Direction is already a colour channel in the data, so the redesign only re-palettes it: tableau10's arbitrary pair becomes cool blue #2F6B9A for below-average decades and warm red #C2352B for above. The hue now agrees with what the value means.", + "Legend dropped. A zero-crossing bar chart with a stated baseline is self-labelling, and the NYT house move is to spend that space on the plot instead of a key.", + "A black zero rule is layered in so the baseline is an explicit reference line rather than an axis artefact — derived from the scale, not from added data.", + "Y grid kept but pushed to #e4e4e4; the x-axis domain line is deleted because the zero rule already plays that role — Flint's default kept both and they fought.", + "Bar width narrowed to 0.72 of the band so the negative and positive runs read as two groups instead of a continuous block.", + "Axis number format switched to signed (+.1f), which does the work the label colour would otherwise have to do.", + "Axis titles removed; the unit moves to a flat '°C' above the y axis, and the title splits into serif line plus sans deck." + ], + "background": "#ffffff", + "padding": { "left": 8, "top": 6, "right": 20, "bottom": 6 }, + "width": 300, + "height": 190, + "title": { + "text": "Global temperature anomaly by decade", + "subtitle": ["°C against the 1951–1980 average"] + }, + "data": { + "values": [ + { "Decade": "1880s", "Anomaly (°C)": -0.17, "Direction": "Below average" }, + { "Decade": "1900s", "Anomaly (°C)": -0.16, "Direction": "Below average" }, + { "Decade": "1920s", "Anomaly (°C)": -0.27, "Direction": "Below average" }, + { "Decade": "1940s", "Anomaly (°C)": 0.12, "Direction": "Above average" }, + { "Decade": "1960s", "Anomaly (°C)": -0.03, "Direction": "Below average" }, + { "Decade": "1980s", "Anomaly (°C)": 0.26, "Direction": "Above average" }, + { "Decade": "2000s", "Anomaly (°C)": 0.4, "Direction": "Above average" }, + { "Decade": "2010s", "Anomaly (°C)": 0.72, "Direction": "Above average" }, + { "Decade": "2020s", "Anomaly (°C)": 1.02, "Direction": "Above average" } + ] + }, + "encoding": { + "x": { "field": "Decade", "type": "nominal", "sort": null, "title": null }, + "y": { + "field": "Anomaly (°C)", + "type": "quantitative", + "title": "°C", + "axis": { "tickCount": 4, "format": "+.1f" } + }, + "color": { + "field": "Direction", + "type": "nominal", + "sort": null, + "scale": { + "domain": ["Below average", "Above average"], + "range": ["#2f6b9a", "#c2352b"] + }, + "legend": null + } + }, + "layer": [ + { + "mark": { "type": "bar", "width": { "band": 0.72 } } + }, + { + "mark": { "type": "rule", "color": "#121212", "strokeWidth": 1 }, + "encoding": { "y": { "datum": 0 }, "x": null, "color": null } + } + ], + "config": { + "background": "#ffffff", + "font": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "title": { + "font": "Georgia, 'Times New Roman', Times, serif", + "fontSize": 15, + "fontWeight": 700, + "color": "#121212", + "anchor": "start", + "offset": 14, + "subtitleFont": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "subtitleFontSize": 11, + "subtitleColor": "#6b6b6b", + "subtitlePadding": 7 + }, + "view": { "stroke": null }, + "axis": { + "labelFont": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "labelFontSize": 10, + "labelColor": "#6b6b6b", + "titleFont": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "titleFontSize": 10, + "titleFontWeight": 400, + "titleColor": "#8e8e8e", + "domain": false, + "ticks": false, + "grid": false + }, + "axisY": { + "grid": true, + "gridColor": "#e4e4e4", + "domain": false, + "ticks": false, + "titleAngle": 0, + "titleAlign": "left", + "titleAnchor": "start", + "titleBaseline": "bottom", + "titleY": -8, + "titlePadding": 0 + }, + "axisX": { + "grid": false, + "domain": false, + "ticks": false, + "labelAngle": 0, + "labelPadding": 4 + } + } +} diff --git a/site/src/playground/theme-lab-assets/temp-heatmap.datawrapper.json b/site/src/playground/theme-lab-assets/temp-heatmap.datawrapper.json new file mode 100644 index 00000000..dbd10de1 --- /dev/null +++ b/site/src/playground/theme-lab-assets/temp-heatmap.datawrapper.json @@ -0,0 +1,396 @@ +{ + "$schema": "https://vega-lite.github.io/schema/vega-lite/v5.json", + "__theme__": "datawrapper", + "__design__": [ + "Ramp cut into five explicit bands at −2, 6, 14 and 22°C rather than left continuous — the same treatment the house gives a choropleth, because a reader can match a swatch to a cell but cannot read a position off a gradient.", + "Key becomes a row of five square swatches above the plot, labelled at the breaks — −2, 6, 14 and 22°C — rather than at the bin centres. The open lower bin carries no label because it has no lower bound to name.", + "Blue-to-red steps with a light neutral at the freezing break, so the crossing point lands on a swatch boundary rather than somewhere inside a gradient.", + "11pt type floor throughout — the chart has to survive being embedded at half width on a phone.", + "Axis domains and ticks deleted; month labels horizontal at 11pt, city names left-aligned in near-black #333.", + "A hairline footer rule closes the chart block, and 1.5px white gutters separate the cells.", + "Label overlap removal switched off on the key. Vega drops colliding tick labels by default, which silently deleted one of the five breaks and left the reader unable to tell which band was which." + ], + "background": "#ffffff", + "padding": { + "left": 10, + "top": 8, + "right": 12, + "bottom": 6 + }, + "title": { + "text": "Average monthly temperature", + "subtitle": [ + "°C, climate normals, four cities" + ], + "anchor": "start", + "font": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "fontSize": 15, + "fontWeight": "bold", + "color": "#333333", + "subtitleFont": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "subtitleFontSize": 11.5, + "subtitleColor": "#666666", + "offset": 10 + }, + "data": { + "values": [ + { + "City": "Singapore", + "Month": "Jan", + "Temp (°C)": 26 + }, + { + "City": "Singapore", + "Month": "Feb", + "Temp (°C)": 27 + }, + { + "City": "Singapore", + "Month": "Mar", + "Temp (°C)": 28 + }, + { + "City": "Singapore", + "Month": "Apr", + "Temp (°C)": 28 + }, + { + "City": "Singapore", + "Month": "May", + "Temp (°C)": 28 + }, + { + "City": "Singapore", + "Month": "Jun", + "Temp (°C)": 28 + }, + { + "City": "Singapore", + "Month": "Jul", + "Temp (°C)": 27 + }, + { + "City": "Singapore", + "Month": "Aug", + "Temp (°C)": 27 + }, + { + "City": "Singapore", + "Month": "Sep", + "Temp (°C)": 27 + }, + { + "City": "Singapore", + "Month": "Oct", + "Temp (°C)": 27 + }, + { + "City": "Singapore", + "Month": "Nov", + "Temp (°C)": 26 + }, + { + "City": "Singapore", + "Month": "Dec", + "Temp (°C)": 26 + }, + { + "City": "Cairo", + "Month": "Jan", + "Temp (°C)": 14 + }, + { + "City": "Cairo", + "Month": "Feb", + "Temp (°C)": 15 + }, + { + "City": "Cairo", + "Month": "Mar", + "Temp (°C)": 18 + }, + { + "City": "Cairo", + "Month": "Apr", + "Temp (°C)": 22 + }, + { + "City": "Cairo", + "Month": "May", + "Temp (°C)": 26 + }, + { + "City": "Cairo", + "Month": "Jun", + "Temp (°C)": 28 + }, + { + "City": "Cairo", + "Month": "Jul", + "Temp (°C)": 29 + }, + { + "City": "Cairo", + "Month": "Aug", + "Temp (°C)": 29 + }, + { + "City": "Cairo", + "Month": "Sep", + "Temp (°C)": 27 + }, + { + "City": "Cairo", + "Month": "Oct", + "Temp (°C)": 24 + }, + { + "City": "Cairo", + "Month": "Nov", + "Temp (°C)": 20 + }, + { + "City": "Cairo", + "Month": "Dec", + "Temp (°C)": 16 + }, + { + "City": "Moscow", + "Month": "Jan", + "Temp (°C)": -9 + }, + { + "City": "Moscow", + "Month": "Feb", + "Temp (°C)": -7 + }, + { + "City": "Moscow", + "Month": "Mar", + "Temp (°C)": -1 + }, + { + "City": "Moscow", + "Month": "Apr", + "Temp (°C)": 7 + }, + { + "City": "Moscow", + "Month": "May", + "Temp (°C)": 13 + }, + { + "City": "Moscow", + "Month": "Jun", + "Temp (°C)": 17 + }, + { + "City": "Moscow", + "Month": "Jul", + "Temp (°C)": 19 + }, + { + "City": "Moscow", + "Month": "Aug", + "Temp (°C)": 17 + }, + { + "City": "Moscow", + "Month": "Sep", + "Temp (°C)": 11 + }, + { + "City": "Moscow", + "Month": "Oct", + "Temp (°C)": 5 + }, + { + "City": "Moscow", + "Month": "Nov", + "Temp (°C)": -1 + }, + { + "City": "Moscow", + "Month": "Dec", + "Temp (°C)": -6 + }, + { + "City": "Seattle", + "Month": "Jan", + "Temp (°C)": 5 + }, + { + "City": "Seattle", + "Month": "Feb", + "Temp (°C)": 6 + }, + { + "City": "Seattle", + "Month": "Mar", + "Temp (°C)": 8 + }, + { + "City": "Seattle", + "Month": "Apr", + "Temp (°C)": 10 + }, + { + "City": "Seattle", + "Month": "May", + "Temp (°C)": 13 + }, + { + "City": "Seattle", + "Month": "Jun", + "Temp (°C)": 16 + }, + { + "City": "Seattle", + "Month": "Jul", + "Temp (°C)": 19 + }, + { + "City": "Seattle", + "Month": "Aug", + "Temp (°C)": 19 + }, + { + "City": "Seattle", + "Month": "Sep", + "Temp (°C)": 16 + }, + { + "City": "Seattle", + "Month": "Oct", + "Temp (°C)": 11 + }, + { + "City": "Seattle", + "Month": "Nov", + "Temp (°C)": 7 + }, + { + "City": "Seattle", + "Month": "Dec", + "Temp (°C)": 4 + } + ] + }, + "vconcat": [ + { + "width": 300, + "height": 116, + "mark": { + "type": "rect", + "stroke": "#ffffff", + "strokeWidth": 1.5 + }, + "encoding": { + "x": { + "field": "Month", + "type": "nominal", + "title": null, + "sort": [ + "Jan", + "Feb", + "Mar", + "Apr", + "May", + "Jun", + "Jul", + "Aug", + "Sep", + "Oct", + "Nov", + "Dec" + ], + "axis": { + "domain": false, + "ticks": false, + "grid": false, + "labelAngle": 0, + "labelPadding": 5, + "labelFontSize": 11, + "labelColor": "#666666" + } + }, + "y": { + "field": "City", + "type": "nominal", + "title": null, + "sort": [ + "Singapore", + "Cairo", + "Moscow", + "Seattle" + ], + "axis": { + "domain": false, + "ticks": false, + "grid": false, + "labelPadding": 7, + "labelFontSize": 11, + "labelColor": "#333333" + } + }, + "color": { + "field": "Temp (°C)", + "type": "quantitative", + "title": null, + "scale": { + "type": "quantize", + "domain": [ + -10, + 30 + ], + "range": [ + "#2f7fa8", + "#a9d3e6", + "#f0ece4", + "#e8ac70", + "#c04a4a" + ] + }, + "legend": { + "orient": "top", + "direction": "horizontal", + "type": "symbol", + "symbolType": "square", + "symbolSize": 170, + "columnPadding": 4, + "offset": 2, + "padding": 0, + "labelOverlap": false, + "format": ".0f", + "labelExpr": "isValid(datum.label) ? datum.label + '°' : ''", + "labelFontSize": 11, + "labelColor": "#333333" + } + } + } + }, + { + "data": { + "values": [ + {} + ] + }, + "mark": { + "type": "rect", + "fill": "#dcdcdc", + "stroke": null + }, + "width": 300, + "height": 1, + "view": { + "stroke": null + } + } + ], + "spacing": 8, + "resolve": { + "legend": { + "color": "independent" + } + } +} diff --git a/site/src/playground/theme-lab-assets/temp-heatmap.economist.json b/site/src/playground/theme-lab-assets/temp-heatmap.economist.json new file mode 100644 index 00000000..93a8766e --- /dev/null +++ b/site/src/playground/theme-lab-assets/temp-heatmap.economist.json @@ -0,0 +1,391 @@ +{ + "$schema": "https://vega-lite.github.io/schema/vega-lite/v5.json", + "__theme__": "economist", + "__design__": [ + "Masthead rule added directly under the headline as its own concat row, sized 26×3 like every other chart in the paper — brand furniture, no data.", + "Diverging ramp assembled entirely from inks the paper already uses on categorical charts: house blue #006BA2 for cold, rust #A1655A for warm, meeting on the paper tint #E9E5DC at 0°C.", + "Key moved to a single horizontal gradient above the plot, where the paper puts every key, with the unit carried as the key's own title rather than an axis title.", + "No numbers printed in the cells. The paper reads a heat grid as a shape — it prints values on a bar because a bar has an end to print at, and a cell does not.", + "Both axis domains and ticks deleted; month and city labels sit tight against the grid at 9pt.", + "Cells separated by a 0.6px white hairline, so adjacent months stay countable without a gridline competing with the fill.", + "Legend resolution forced to independent so the key stays inside the plot row instead of being hoisted above the masthead by the concat container." + ], + "background": "#ffffff", + "padding": { + "left": 10, + "top": 6, + "right": 12, + "bottom": 8 + }, + "title": { + "text": "Average monthly temperature", + "subtitle": [ + "°C, climate normals, four cities" + ], + "anchor": "start", + "font": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "fontSize": 14, + "fontWeight": "bold", + "color": "#121317", + "subtitleFont": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "subtitleFontSize": 11, + "subtitleColor": "#54585a", + "offset": 8 + }, + "data": { + "values": [ + { + "City": "Singapore", + "Month": "Jan", + "Temp (°C)": 26 + }, + { + "City": "Singapore", + "Month": "Feb", + "Temp (°C)": 27 + }, + { + "City": "Singapore", + "Month": "Mar", + "Temp (°C)": 28 + }, + { + "City": "Singapore", + "Month": "Apr", + "Temp (°C)": 28 + }, + { + "City": "Singapore", + "Month": "May", + "Temp (°C)": 28 + }, + { + "City": "Singapore", + "Month": "Jun", + "Temp (°C)": 28 + }, + { + "City": "Singapore", + "Month": "Jul", + "Temp (°C)": 27 + }, + { + "City": "Singapore", + "Month": "Aug", + "Temp (°C)": 27 + }, + { + "City": "Singapore", + "Month": "Sep", + "Temp (°C)": 27 + }, + { + "City": "Singapore", + "Month": "Oct", + "Temp (°C)": 27 + }, + { + "City": "Singapore", + "Month": "Nov", + "Temp (°C)": 26 + }, + { + "City": "Singapore", + "Month": "Dec", + "Temp (°C)": 26 + }, + { + "City": "Cairo", + "Month": "Jan", + "Temp (°C)": 14 + }, + { + "City": "Cairo", + "Month": "Feb", + "Temp (°C)": 15 + }, + { + "City": "Cairo", + "Month": "Mar", + "Temp (°C)": 18 + }, + { + "City": "Cairo", + "Month": "Apr", + "Temp (°C)": 22 + }, + { + "City": "Cairo", + "Month": "May", + "Temp (°C)": 26 + }, + { + "City": "Cairo", + "Month": "Jun", + "Temp (°C)": 28 + }, + { + "City": "Cairo", + "Month": "Jul", + "Temp (°C)": 29 + }, + { + "City": "Cairo", + "Month": "Aug", + "Temp (°C)": 29 + }, + { + "City": "Cairo", + "Month": "Sep", + "Temp (°C)": 27 + }, + { + "City": "Cairo", + "Month": "Oct", + "Temp (°C)": 24 + }, + { + "City": "Cairo", + "Month": "Nov", + "Temp (°C)": 20 + }, + { + "City": "Cairo", + "Month": "Dec", + "Temp (°C)": 16 + }, + { + "City": "Moscow", + "Month": "Jan", + "Temp (°C)": -9 + }, + { + "City": "Moscow", + "Month": "Feb", + "Temp (°C)": -7 + }, + { + "City": "Moscow", + "Month": "Mar", + "Temp (°C)": -1 + }, + { + "City": "Moscow", + "Month": "Apr", + "Temp (°C)": 7 + }, + { + "City": "Moscow", + "Month": "May", + "Temp (°C)": 13 + }, + { + "City": "Moscow", + "Month": "Jun", + "Temp (°C)": 17 + }, + { + "City": "Moscow", + "Month": "Jul", + "Temp (°C)": 19 + }, + { + "City": "Moscow", + "Month": "Aug", + "Temp (°C)": 17 + }, + { + "City": "Moscow", + "Month": "Sep", + "Temp (°C)": 11 + }, + { + "City": "Moscow", + "Month": "Oct", + "Temp (°C)": 5 + }, + { + "City": "Moscow", + "Month": "Nov", + "Temp (°C)": -1 + }, + { + "City": "Moscow", + "Month": "Dec", + "Temp (°C)": -6 + }, + { + "City": "Seattle", + "Month": "Jan", + "Temp (°C)": 5 + }, + { + "City": "Seattle", + "Month": "Feb", + "Temp (°C)": 6 + }, + { + "City": "Seattle", + "Month": "Mar", + "Temp (°C)": 8 + }, + { + "City": "Seattle", + "Month": "Apr", + "Temp (°C)": 10 + }, + { + "City": "Seattle", + "Month": "May", + "Temp (°C)": 13 + }, + { + "City": "Seattle", + "Month": "Jun", + "Temp (°C)": 16 + }, + { + "City": "Seattle", + "Month": "Jul", + "Temp (°C)": 19 + }, + { + "City": "Seattle", + "Month": "Aug", + "Temp (°C)": 19 + }, + { + "City": "Seattle", + "Month": "Sep", + "Temp (°C)": 16 + }, + { + "City": "Seattle", + "Month": "Oct", + "Temp (°C)": 11 + }, + { + "City": "Seattle", + "Month": "Nov", + "Temp (°C)": 7 + }, + { + "City": "Seattle", + "Month": "Dec", + "Temp (°C)": 4 + } + ] + }, + "vconcat": [ + { + "data": { + "values": [ + {} + ] + }, + "mark": { + "type": "rect", + "fill": "#e3120b", + "stroke": null + }, + "width": 26, + "height": 3, + "view": { + "stroke": null + } + }, + { + "width": 288, + "height": 112, + "mark": { + "type": "rect", + "stroke": "#ffffff", + "strokeWidth": 0.6 + }, + "encoding": { + "x": { + "field": "Month", + "type": "nominal", + "title": null, + "sort": [ + "Jan", + "Feb", + "Mar", + "Apr", + "May", + "Jun", + "Jul", + "Aug", + "Sep", + "Oct", + "Nov", + "Dec" + ], + "axis": { + "domain": false, + "ticks": false, + "grid": false, + "labelAngle": 0, + "labelPadding": 4, + "labelFontSize": 9, + "labelColor": "#54585a" + } + }, + "y": { + "field": "City", + "type": "nominal", + "title": null, + "sort": [ + "Singapore", + "Cairo", + "Moscow", + "Seattle" + ], + "axis": { + "domain": false, + "ticks": false, + "grid": false, + "labelPadding": 6, + "labelFontSize": 9, + "labelColor": "#121317" + } + }, + "color": { + "field": "Temp (°C)", + "type": "quantitative", + "title": "°C", + "scale": { + "domainMid": 0, + "range": [ + "#006ba2", + "#7ba7b8", + "#e9e5dc", + "#c8967a", + "#a1655a" + ] + }, + "legend": { + "orient": "top", + "direction": "horizontal", + "gradientLength": 140, + "gradientThickness": 7, + "titleOrient": "left", + "offset": 4, + "format": ".0f", + "labelFontSize": 9, + "labelColor": "#54585a", + "titleFontSize": 9, + "titleColor": "#54585a", + "padding": 0 + } + } + } + } + ], + "spacing": 8, + "resolve": { + "legend": { + "color": "independent" + } + } +} diff --git a/site/src/playground/theme-lab-assets/temp-heatmap.flint.json b/site/src/playground/theme-lab-assets/temp-heatmap.flint.json new file mode 100644 index 00000000..e83071de --- /dev/null +++ b/site/src/playground/theme-lab-assets/temp-heatmap.flint.json @@ -0,0 +1,318 @@ +{ + "mark": "rect", + "encoding": { + "x": { + "field": "Month", + "type": "nominal", + "sort": [ + "Jan", + "Feb", + "Mar", + "Apr", + "May", + "Jun", + "Jul", + "Aug", + "Sep", + "Oct", + "Nov", + "Dec" + ] + }, + "y": { + "field": "City", + "type": "nominal", + "sort": null + }, + "color": { + "field": "Temp (°C)", + "type": "quantitative", + "scale": { + "scheme": "redblue", + "domainMid": 0, + "domain": [ + -29, + 29 + ] + } + } + }, + "config": { + "view": { + "continuousWidth": 340, + "continuousHeight": 230 + }, + "axisX": { + "labelLimit": 100, + "labelFontSize": 10, + "titleFontSize": 11 + }, + "axisY": { + "labelFontSize": 10, + "titleFontSize": 11 + }, + "legend": { + "labelFontSize": 10, + "titleFontSize": 11 + }, + "facet": { + "spacing": 23 + } + }, + "width": { + "step": 23 + }, + "height": { + "step": 23 + }, + "data": { + "values": [ + { + "City": "Singapore", + "Month": "Jan", + "Temp (°C)": 26 + }, + { + "City": "Singapore", + "Month": "Feb", + "Temp (°C)": 27 + }, + { + "City": "Singapore", + "Month": "Mar", + "Temp (°C)": 28 + }, + { + "City": "Singapore", + "Month": "Apr", + "Temp (°C)": 28 + }, + { + "City": "Singapore", + "Month": "May", + "Temp (°C)": 28 + }, + { + "City": "Singapore", + "Month": "Jun", + "Temp (°C)": 28 + }, + { + "City": "Singapore", + "Month": "Jul", + "Temp (°C)": 27 + }, + { + "City": "Singapore", + "Month": "Aug", + "Temp (°C)": 27 + }, + { + "City": "Singapore", + "Month": "Sep", + "Temp (°C)": 27 + }, + { + "City": "Singapore", + "Month": "Oct", + "Temp (°C)": 27 + }, + { + "City": "Singapore", + "Month": "Nov", + "Temp (°C)": 26 + }, + { + "City": "Singapore", + "Month": "Dec", + "Temp (°C)": 26 + }, + { + "City": "Cairo", + "Month": "Jan", + "Temp (°C)": 14 + }, + { + "City": "Cairo", + "Month": "Feb", + "Temp (°C)": 15 + }, + { + "City": "Cairo", + "Month": "Mar", + "Temp (°C)": 18 + }, + { + "City": "Cairo", + "Month": "Apr", + "Temp (°C)": 22 + }, + { + "City": "Cairo", + "Month": "May", + "Temp (°C)": 26 + }, + { + "City": "Cairo", + "Month": "Jun", + "Temp (°C)": 28 + }, + { + "City": "Cairo", + "Month": "Jul", + "Temp (°C)": 29 + }, + { + "City": "Cairo", + "Month": "Aug", + "Temp (°C)": 29 + }, + { + "City": "Cairo", + "Month": "Sep", + "Temp (°C)": 27 + }, + { + "City": "Cairo", + "Month": "Oct", + "Temp (°C)": 24 + }, + { + "City": "Cairo", + "Month": "Nov", + "Temp (°C)": 20 + }, + { + "City": "Cairo", + "Month": "Dec", + "Temp (°C)": 16 + }, + { + "City": "Moscow", + "Month": "Jan", + "Temp (°C)": -9 + }, + { + "City": "Moscow", + "Month": "Feb", + "Temp (°C)": -7 + }, + { + "City": "Moscow", + "Month": "Mar", + "Temp (°C)": -1 + }, + { + "City": "Moscow", + "Month": "Apr", + "Temp (°C)": 7 + }, + { + "City": "Moscow", + "Month": "May", + "Temp (°C)": 13 + }, + { + "City": "Moscow", + "Month": "Jun", + "Temp (°C)": 17 + }, + { + "City": "Moscow", + "Month": "Jul", + "Temp (°C)": 19 + }, + { + "City": "Moscow", + "Month": "Aug", + "Temp (°C)": 17 + }, + { + "City": "Moscow", + "Month": "Sep", + "Temp (°C)": 11 + }, + { + "City": "Moscow", + "Month": "Oct", + "Temp (°C)": 5 + }, + { + "City": "Moscow", + "Month": "Nov", + "Temp (°C)": -1 + }, + { + "City": "Moscow", + "Month": "Dec", + "Temp (°C)": -6 + }, + { + "City": "Seattle", + "Month": "Jan", + "Temp (°C)": 5 + }, + { + "City": "Seattle", + "Month": "Feb", + "Temp (°C)": 6 + }, + { + "City": "Seattle", + "Month": "Mar", + "Temp (°C)": 8 + }, + { + "City": "Seattle", + "Month": "Apr", + "Temp (°C)": 10 + }, + { + "City": "Seattle", + "Month": "May", + "Temp (°C)": 13 + }, + { + "City": "Seattle", + "Month": "Jun", + "Temp (°C)": 16 + }, + { + "City": "Seattle", + "Month": "Jul", + "Temp (°C)": 19 + }, + { + "City": "Seattle", + "Month": "Aug", + "Temp (°C)": 19 + }, + { + "City": "Seattle", + "Month": "Sep", + "Temp (°C)": 16 + }, + { + "City": "Seattle", + "Month": "Oct", + "Temp (°C)": 11 + }, + { + "City": "Seattle", + "Month": "Nov", + "Temp (°C)": 7 + }, + { + "City": "Seattle", + "Month": "Dec", + "Temp (°C)": 4 + } + ] + }, + "title": { + "text": "Average monthly temperature", + "subtitle": [ + "°C, climate normals, four cities" + ] + } +} diff --git a/site/src/playground/theme-lab-assets/temp-heatmap.mckinsey.json b/site/src/playground/theme-lab-assets/temp-heatmap.mckinsey.json new file mode 100644 index 00000000..a3758445 --- /dev/null +++ b/site/src/playground/theme-lab-assets/temp-heatmap.mckinsey.json @@ -0,0 +1,378 @@ +{ + "$schema": "https://vega-lite.github.io/schema/vega-lite/v5.json", + "__theme__": "mckinsey", + "__design__": [ + "The house has no warm ink, so the diverging ramp is dropped for a single-hue navy wash. Below-freezing stops being a colour state and becomes a number like any other.", + "Every cell prints its value. Once the numbers are on the page the colour is doing ordering work only, and it is pushed back to a pale wash so the digits sit clearly on top.", + "Key deleted. A gradient bar next to a grid of printed numbers restates what the numbers already say.", + "Both axes stripped of domain, ticks and titles; city names left-aligned in navy, months in a lighter grey above the grid.", + "Numbers reverse out to white on the four darkest steps and stay navy elsewhere — the switch is on the value, not on the row.", + "Generous 26px cells with a 0.8px white gutter — the grid is spaced to be read as a table, which is what it now is." + ], + "background": "#ffffff", + "padding": { + "left": 10, + "top": 8, + "right": 12, + "bottom": 8 + }, + "width": 312, + "height": 104, + "title": { + "text": "Average monthly temperature", + "subtitle": [ + "°C, climate normals, four cities" + ], + "anchor": "start", + "font": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "fontSize": 14, + "fontWeight": "bold", + "color": "#051c2c", + "subtitleFont": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "subtitleFontSize": 11, + "subtitleColor": "#5a6872", + "offset": 10 + }, + "data": { + "values": [ + { + "City": "Singapore", + "Month": "Jan", + "Temp (°C)": 26 + }, + { + "City": "Singapore", + "Month": "Feb", + "Temp (°C)": 27 + }, + { + "City": "Singapore", + "Month": "Mar", + "Temp (°C)": 28 + }, + { + "City": "Singapore", + "Month": "Apr", + "Temp (°C)": 28 + }, + { + "City": "Singapore", + "Month": "May", + "Temp (°C)": 28 + }, + { + "City": "Singapore", + "Month": "Jun", + "Temp (°C)": 28 + }, + { + "City": "Singapore", + "Month": "Jul", + "Temp (°C)": 27 + }, + { + "City": "Singapore", + "Month": "Aug", + "Temp (°C)": 27 + }, + { + "City": "Singapore", + "Month": "Sep", + "Temp (°C)": 27 + }, + { + "City": "Singapore", + "Month": "Oct", + "Temp (°C)": 27 + }, + { + "City": "Singapore", + "Month": "Nov", + "Temp (°C)": 26 + }, + { + "City": "Singapore", + "Month": "Dec", + "Temp (°C)": 26 + }, + { + "City": "Cairo", + "Month": "Jan", + "Temp (°C)": 14 + }, + { + "City": "Cairo", + "Month": "Feb", + "Temp (°C)": 15 + }, + { + "City": "Cairo", + "Month": "Mar", + "Temp (°C)": 18 + }, + { + "City": "Cairo", + "Month": "Apr", + "Temp (°C)": 22 + }, + { + "City": "Cairo", + "Month": "May", + "Temp (°C)": 26 + }, + { + "City": "Cairo", + "Month": "Jun", + "Temp (°C)": 28 + }, + { + "City": "Cairo", + "Month": "Jul", + "Temp (°C)": 29 + }, + { + "City": "Cairo", + "Month": "Aug", + "Temp (°C)": 29 + }, + { + "City": "Cairo", + "Month": "Sep", + "Temp (°C)": 27 + }, + { + "City": "Cairo", + "Month": "Oct", + "Temp (°C)": 24 + }, + { + "City": "Cairo", + "Month": "Nov", + "Temp (°C)": 20 + }, + { + "City": "Cairo", + "Month": "Dec", + "Temp (°C)": 16 + }, + { + "City": "Moscow", + "Month": "Jan", + "Temp (°C)": -9 + }, + { + "City": "Moscow", + "Month": "Feb", + "Temp (°C)": -7 + }, + { + "City": "Moscow", + "Month": "Mar", + "Temp (°C)": -1 + }, + { + "City": "Moscow", + "Month": "Apr", + "Temp (°C)": 7 + }, + { + "City": "Moscow", + "Month": "May", + "Temp (°C)": 13 + }, + { + "City": "Moscow", + "Month": "Jun", + "Temp (°C)": 17 + }, + { + "City": "Moscow", + "Month": "Jul", + "Temp (°C)": 19 + }, + { + "City": "Moscow", + "Month": "Aug", + "Temp (°C)": 17 + }, + { + "City": "Moscow", + "Month": "Sep", + "Temp (°C)": 11 + }, + { + "City": "Moscow", + "Month": "Oct", + "Temp (°C)": 5 + }, + { + "City": "Moscow", + "Month": "Nov", + "Temp (°C)": -1 + }, + { + "City": "Moscow", + "Month": "Dec", + "Temp (°C)": -6 + }, + { + "City": "Seattle", + "Month": "Jan", + "Temp (°C)": 5 + }, + { + "City": "Seattle", + "Month": "Feb", + "Temp (°C)": 6 + }, + { + "City": "Seattle", + "Month": "Mar", + "Temp (°C)": 8 + }, + { + "City": "Seattle", + "Month": "Apr", + "Temp (°C)": 10 + }, + { + "City": "Seattle", + "Month": "May", + "Temp (°C)": 13 + }, + { + "City": "Seattle", + "Month": "Jun", + "Temp (°C)": 16 + }, + { + "City": "Seattle", + "Month": "Jul", + "Temp (°C)": 19 + }, + { + "City": "Seattle", + "Month": "Aug", + "Temp (°C)": 19 + }, + { + "City": "Seattle", + "Month": "Sep", + "Temp (°C)": 16 + }, + { + "City": "Seattle", + "Month": "Oct", + "Temp (°C)": 11 + }, + { + "City": "Seattle", + "Month": "Nov", + "Temp (°C)": 7 + }, + { + "City": "Seattle", + "Month": "Dec", + "Temp (°C)": 4 + } + ] + }, + "encoding": { + "x": { + "field": "Month", + "type": "nominal", + "title": null, + "sort": [ + "Jan", + "Feb", + "Mar", + "Apr", + "May", + "Jun", + "Jul", + "Aug", + "Sep", + "Oct", + "Nov", + "Dec" + ], + "axis": { + "domain": false, + "ticks": false, + "grid": false, + "labelAngle": 0, + "labelPadding": 5, + "labelFontSize": 10, + "labelColor": "#8a969d", + "orient": "top" + } + }, + "y": { + "field": "City", + "type": "nominal", + "title": null, + "sort": [ + "Singapore", + "Cairo", + "Moscow", + "Seattle" + ], + "axis": { + "domain": false, + "ticks": false, + "grid": false, + "labelPadding": 8, + "labelFontSize": 11, + "labelColor": "#051c2c" + } + } + }, + "layer": [ + { + "mark": { + "type": "rect", + "stroke": "#ffffff", + "strokeWidth": 0.8 + }, + "encoding": { + "color": { + "field": "Temp (°C)", + "type": "quantitative", + "title": null, + "scale": { + "range": [ + "#eef3f8", + "#cfdcea", + "#9db8d2", + "#5b82ab", + "#051c2c" + ] + }, + "legend": null + } + } + }, + { + "mark": { + "type": "text", + "font": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "fontSize": 10, + "fontWeight": 600 + }, + "encoding": { + "text": { + "field": "Temp (°C)", + "type": "quantitative", + "format": ".0f" + }, + "color": { + "condition": { + "test": "datum['Temp (°C)'] >= 19", + "value": "#ffffff" + }, + "value": "#051c2c" + } + } + } + ] +} diff --git a/site/src/playground/theme-lab-assets/temp-heatmap.nature.json b/site/src/playground/theme-lab-assets/temp-heatmap.nature.json new file mode 100644 index 00000000..49fd7607 --- /dev/null +++ b/site/src/playground/theme-lab-assets/temp-heatmap.nature.json @@ -0,0 +1,387 @@ +{ + "$schema": "https://vega-lite.github.io/schema/vega-lite/v5.json", + "__theme__": "nature", + "__design__": [ + "Ramp replaced with the Okabe-Ito diverging pair — #0072B2 to #D55E00 through white — which stays separable under deuteranopia and protanopia. A red-blue ramp does not.", + "Colour bar moved to the right as a vertical gradient with outward ticks and its own title carrying the unit, so the figure states its scale the way every other axis in the journal does.", + "Both axes keep their black domain line and outward ticks; the L-shaped spine is the same rule applied here as on every other panel.", + "Axis titles restored and named with units — a journal figure must be readable out of its caption.", + "Type dropped to 8.5pt Arial in pure black throughout, sized for an 89mm single column.", + "Cells left unstroked. The white gutter other houses use would be read as data on a diverging ramp whose midpoint is also white." + ], + "background": "#ffffff", + "padding": { + "left": 10, + "top": 8, + "right": 10, + "bottom": 8 + }, + "width": 252, + "height": 98, + "title": { + "text": "Average monthly temperature", + "subtitle": [ + "°C, climate normals, four cities" + ], + "anchor": "start", + "font": "Arial, Helvetica, sans-serif", + "fontSize": 11.5, + "fontWeight": "bold", + "color": "#000000", + "subtitleFont": "Arial, Helvetica, sans-serif", + "subtitleFontSize": 9, + "subtitleColor": "#000000", + "offset": 8 + }, + "data": { + "values": [ + { + "City": "Singapore", + "Month": "Jan", + "Temp (°C)": 26 + }, + { + "City": "Singapore", + "Month": "Feb", + "Temp (°C)": 27 + }, + { + "City": "Singapore", + "Month": "Mar", + "Temp (°C)": 28 + }, + { + "City": "Singapore", + "Month": "Apr", + "Temp (°C)": 28 + }, + { + "City": "Singapore", + "Month": "May", + "Temp (°C)": 28 + }, + { + "City": "Singapore", + "Month": "Jun", + "Temp (°C)": 28 + }, + { + "City": "Singapore", + "Month": "Jul", + "Temp (°C)": 27 + }, + { + "City": "Singapore", + "Month": "Aug", + "Temp (°C)": 27 + }, + { + "City": "Singapore", + "Month": "Sep", + "Temp (°C)": 27 + }, + { + "City": "Singapore", + "Month": "Oct", + "Temp (°C)": 27 + }, + { + "City": "Singapore", + "Month": "Nov", + "Temp (°C)": 26 + }, + { + "City": "Singapore", + "Month": "Dec", + "Temp (°C)": 26 + }, + { + "City": "Cairo", + "Month": "Jan", + "Temp (°C)": 14 + }, + { + "City": "Cairo", + "Month": "Feb", + "Temp (°C)": 15 + }, + { + "City": "Cairo", + "Month": "Mar", + "Temp (°C)": 18 + }, + { + "City": "Cairo", + "Month": "Apr", + "Temp (°C)": 22 + }, + { + "City": "Cairo", + "Month": "May", + "Temp (°C)": 26 + }, + { + "City": "Cairo", + "Month": "Jun", + "Temp (°C)": 28 + }, + { + "City": "Cairo", + "Month": "Jul", + "Temp (°C)": 29 + }, + { + "City": "Cairo", + "Month": "Aug", + "Temp (°C)": 29 + }, + { + "City": "Cairo", + "Month": "Sep", + "Temp (°C)": 27 + }, + { + "City": "Cairo", + "Month": "Oct", + "Temp (°C)": 24 + }, + { + "City": "Cairo", + "Month": "Nov", + "Temp (°C)": 20 + }, + { + "City": "Cairo", + "Month": "Dec", + "Temp (°C)": 16 + }, + { + "City": "Moscow", + "Month": "Jan", + "Temp (°C)": -9 + }, + { + "City": "Moscow", + "Month": "Feb", + "Temp (°C)": -7 + }, + { + "City": "Moscow", + "Month": "Mar", + "Temp (°C)": -1 + }, + { + "City": "Moscow", + "Month": "Apr", + "Temp (°C)": 7 + }, + { + "City": "Moscow", + "Month": "May", + "Temp (°C)": 13 + }, + { + "City": "Moscow", + "Month": "Jun", + "Temp (°C)": 17 + }, + { + "City": "Moscow", + "Month": "Jul", + "Temp (°C)": 19 + }, + { + "City": "Moscow", + "Month": "Aug", + "Temp (°C)": 17 + }, + { + "City": "Moscow", + "Month": "Sep", + "Temp (°C)": 11 + }, + { + "City": "Moscow", + "Month": "Oct", + "Temp (°C)": 5 + }, + { + "City": "Moscow", + "Month": "Nov", + "Temp (°C)": -1 + }, + { + "City": "Moscow", + "Month": "Dec", + "Temp (°C)": -6 + }, + { + "City": "Seattle", + "Month": "Jan", + "Temp (°C)": 5 + }, + { + "City": "Seattle", + "Month": "Feb", + "Temp (°C)": 6 + }, + { + "City": "Seattle", + "Month": "Mar", + "Temp (°C)": 8 + }, + { + "City": "Seattle", + "Month": "Apr", + "Temp (°C)": 10 + }, + { + "City": "Seattle", + "Month": "May", + "Temp (°C)": 13 + }, + { + "City": "Seattle", + "Month": "Jun", + "Temp (°C)": 16 + }, + { + "City": "Seattle", + "Month": "Jul", + "Temp (°C)": 19 + }, + { + "City": "Seattle", + "Month": "Aug", + "Temp (°C)": 19 + }, + { + "City": "Seattle", + "Month": "Sep", + "Temp (°C)": 16 + }, + { + "City": "Seattle", + "Month": "Oct", + "Temp (°C)": 11 + }, + { + "City": "Seattle", + "Month": "Nov", + "Temp (°C)": 7 + }, + { + "City": "Seattle", + "Month": "Dec", + "Temp (°C)": 4 + } + ] + }, + "mark": { + "type": "rect" + }, + "encoding": { + "x": { + "field": "Month", + "type": "nominal", + "title": "Month", + "sort": [ + "Jan", + "Feb", + "Mar", + "Apr", + "May", + "Jun", + "Jul", + "Aug", + "Sep", + "Oct", + "Nov", + "Dec" + ], + "axis": { + "domain": true, + "domainColor": "#000000", + "domainWidth": 0.8, + "ticks": true, + "tickColor": "#000000", + "tickWidth": 0.8, + "tickSize": 4, + "grid": false, + "labelAngle": 0, + "labelPadding": 2, + "labelFont": "Arial, Helvetica, sans-serif", + "labelFontSize": 8.5, + "labelColor": "#000000", + "titleFont": "Arial, Helvetica, sans-serif", + "titleFontSize": 8.5, + "titleColor": "#000000", + "titlePadding": 4 + } + }, + "y": { + "field": "City", + "type": "nominal", + "title": "City", + "sort": [ + "Singapore", + "Cairo", + "Moscow", + "Seattle" + ], + "axis": { + "domain": true, + "domainColor": "#000000", + "domainWidth": 0.8, + "ticks": true, + "tickColor": "#000000", + "tickWidth": 0.8, + "tickSize": 4, + "grid": false, + "labelPadding": 2, + "labelFont": "Arial, Helvetica, sans-serif", + "labelFontSize": 8.5, + "labelColor": "#000000", + "titleFont": "Arial, Helvetica, sans-serif", + "titleFontSize": 8.5, + "titleColor": "#000000", + "titlePadding": 4 + } + }, + "color": { + "field": "Temp (°C)", + "type": "quantitative", + "title": "Temperature (°C)", + "scale": { + "domainMid": 0, + "range": [ + "#0072b2", + "#83b9db", + "#ffffff", + "#eba06a", + "#d55e00" + ] + }, + "legend": { + "orient": "right", + "direction": "vertical", + "gradientLength": 90, + "gradientThickness": 8, + "titleOrient": "left", + "titleAlign": "center", + "titleAnchor": "middle", + "offset": 8, + "format": ".0f", + "labelFont": "Arial, Helvetica, sans-serif", + "labelFontSize": 8.5, + "labelColor": "#000000", + "titleFont": "Arial, Helvetica, sans-serif", + "titleFontSize": 8.5, + "titleColor": "#000000", + "tickCount": 5, + "gradientStrokeColor": "#000000", + "gradientStrokeWidth": 0.5 + } + } + } +} diff --git a/site/src/playground/theme-lab-assets/temp-heatmap.nyt.json b/site/src/playground/theme-lab-assets/temp-heatmap.nyt.json new file mode 100644 index 00000000..30c17623 --- /dev/null +++ b/site/src/playground/theme-lab-assets/temp-heatmap.nyt.json @@ -0,0 +1,397 @@ +{ + "$schema": "https://vega-lite.github.io/schema/vega-lite/v5.json", + "__theme__": "nyt", + "__design__": [ + "A continuous ramp cannot be direct-labelled, so the house's preferred device is unavailable and the legend it would normally displace survives - a short horizontal gradient above the plot, ticked at the two ends and at freezing.", + "Ramp rebuilt from the two house inks already used for signed measures: #2F6B9A for cold, #C2352B for warm, meeting at a warm paper grey at 0°C. Freezing is the pivot, so it gets the neutral.", + "Every cell prints its number. The house prints values wherever they fit, and a 23px cell fits two digits — the shade orders the grid, the number answers it.", + "Label ink reverses to white only on the four darkest steps at either end of the ramp. The switch is on distance from the midpoint, not on the sign of the value — a −1°C cell is nearly white and needs black type.", + "Serif headline flush left, axis domains and ticks removed, month and city labels in Helvetica — the grid itself is the only structure the chart needs.", + "1px white gutters between cells so the grid reads as cells rather than as a continuous field." + ], + "background": "#ffffff", + "padding": { + "left": 10, + "top": 8, + "right": 12, + "bottom": 8 + }, + "width": 288, + "height": 112, + "title": { + "text": "Average monthly temperature", + "subtitle": [ + "°C, climate normals, four cities" + ], + "anchor": "start", + "font": "Georgia, 'Times New Roman', serif", + "fontSize": 15, + "fontWeight": "bold", + "color": "#121212", + "subtitleFont": "Georgia, 'Times New Roman', serif", + "subtitleFontSize": 11.5, + "subtitleColor": "#6b6b6b", + "offset": 10 + }, + "data": { + "values": [ + { + "City": "Singapore", + "Month": "Jan", + "Temp (°C)": 26 + }, + { + "City": "Singapore", + "Month": "Feb", + "Temp (°C)": 27 + }, + { + "City": "Singapore", + "Month": "Mar", + "Temp (°C)": 28 + }, + { + "City": "Singapore", + "Month": "Apr", + "Temp (°C)": 28 + }, + { + "City": "Singapore", + "Month": "May", + "Temp (°C)": 28 + }, + { + "City": "Singapore", + "Month": "Jun", + "Temp (°C)": 28 + }, + { + "City": "Singapore", + "Month": "Jul", + "Temp (°C)": 27 + }, + { + "City": "Singapore", + "Month": "Aug", + "Temp (°C)": 27 + }, + { + "City": "Singapore", + "Month": "Sep", + "Temp (°C)": 27 + }, + { + "City": "Singapore", + "Month": "Oct", + "Temp (°C)": 27 + }, + { + "City": "Singapore", + "Month": "Nov", + "Temp (°C)": 26 + }, + { + "City": "Singapore", + "Month": "Dec", + "Temp (°C)": 26 + }, + { + "City": "Cairo", + "Month": "Jan", + "Temp (°C)": 14 + }, + { + "City": "Cairo", + "Month": "Feb", + "Temp (°C)": 15 + }, + { + "City": "Cairo", + "Month": "Mar", + "Temp (°C)": 18 + }, + { + "City": "Cairo", + "Month": "Apr", + "Temp (°C)": 22 + }, + { + "City": "Cairo", + "Month": "May", + "Temp (°C)": 26 + }, + { + "City": "Cairo", + "Month": "Jun", + "Temp (°C)": 28 + }, + { + "City": "Cairo", + "Month": "Jul", + "Temp (°C)": 29 + }, + { + "City": "Cairo", + "Month": "Aug", + "Temp (°C)": 29 + }, + { + "City": "Cairo", + "Month": "Sep", + "Temp (°C)": 27 + }, + { + "City": "Cairo", + "Month": "Oct", + "Temp (°C)": 24 + }, + { + "City": "Cairo", + "Month": "Nov", + "Temp (°C)": 20 + }, + { + "City": "Cairo", + "Month": "Dec", + "Temp (°C)": 16 + }, + { + "City": "Moscow", + "Month": "Jan", + "Temp (°C)": -9 + }, + { + "City": "Moscow", + "Month": "Feb", + "Temp (°C)": -7 + }, + { + "City": "Moscow", + "Month": "Mar", + "Temp (°C)": -1 + }, + { + "City": "Moscow", + "Month": "Apr", + "Temp (°C)": 7 + }, + { + "City": "Moscow", + "Month": "May", + "Temp (°C)": 13 + }, + { + "City": "Moscow", + "Month": "Jun", + "Temp (°C)": 17 + }, + { + "City": "Moscow", + "Month": "Jul", + "Temp (°C)": 19 + }, + { + "City": "Moscow", + "Month": "Aug", + "Temp (°C)": 17 + }, + { + "City": "Moscow", + "Month": "Sep", + "Temp (°C)": 11 + }, + { + "City": "Moscow", + "Month": "Oct", + "Temp (°C)": 5 + }, + { + "City": "Moscow", + "Month": "Nov", + "Temp (°C)": -1 + }, + { + "City": "Moscow", + "Month": "Dec", + "Temp (°C)": -6 + }, + { + "City": "Seattle", + "Month": "Jan", + "Temp (°C)": 5 + }, + { + "City": "Seattle", + "Month": "Feb", + "Temp (°C)": 6 + }, + { + "City": "Seattle", + "Month": "Mar", + "Temp (°C)": 8 + }, + { + "City": "Seattle", + "Month": "Apr", + "Temp (°C)": 10 + }, + { + "City": "Seattle", + "Month": "May", + "Temp (°C)": 13 + }, + { + "City": "Seattle", + "Month": "Jun", + "Temp (°C)": 16 + }, + { + "City": "Seattle", + "Month": "Jul", + "Temp (°C)": 19 + }, + { + "City": "Seattle", + "Month": "Aug", + "Temp (°C)": 19 + }, + { + "City": "Seattle", + "Month": "Sep", + "Temp (°C)": 16 + }, + { + "City": "Seattle", + "Month": "Oct", + "Temp (°C)": 11 + }, + { + "City": "Seattle", + "Month": "Nov", + "Temp (°C)": 7 + }, + { + "City": "Seattle", + "Month": "Dec", + "Temp (°C)": 4 + } + ] + }, + "encoding": { + "x": { + "field": "Month", + "type": "nominal", + "title": null, + "sort": [ + "Jan", + "Feb", + "Mar", + "Apr", + "May", + "Jun", + "Jul", + "Aug", + "Sep", + "Oct", + "Nov", + "Dec" + ], + "axis": { + "domain": false, + "ticks": false, + "grid": false, + "labelAngle": 0, + "labelPadding": 4, + "labelFont": "Helvetica, Arial, sans-serif", + "labelFontSize": 10, + "labelColor": "#121212" + } + }, + "y": { + "field": "City", + "type": "nominal", + "title": null, + "sort": [ + "Singapore", + "Cairo", + "Moscow", + "Seattle" + ], + "axis": { + "domain": false, + "ticks": false, + "grid": false, + "labelPadding": 6, + "labelFont": "Helvetica, Arial, sans-serif", + "labelFontSize": 10, + "labelColor": "#121212" + } + } + }, + "layer": [ + { + "mark": { + "type": "rect", + "stroke": "#ffffff", + "strokeWidth": 1 + }, + "encoding": { + "color": { + "field": "Temp (°C)", + "type": "quantitative", + "title": null, + "scale": { + "domainMid": 0, + "range": [ + "#2f6b9a", + "#8fb4cc", + "#efece5", + "#dd9a86", + "#c2352b" + ] + }, + "legend": { + "orient": "top", + "direction": "horizontal", + "gradientLength": 150, + "gradientThickness": 7, + "titleOrient": "left", + "offset": 6, + "format": ".0f", + "labelFont": "Helvetica, Arial, sans-serif", + "labelFontSize": 10, + "labelColor": "#6b6b6b", + "padding": 0, + "values": [ + -9, + 0, + 29 + ], + "labelExpr": "datum.label + '°'" + } + } + } + }, + { + "mark": { + "type": "text", + "font": "Helvetica, Arial, sans-serif", + "fontSize": 8.5 + }, + "encoding": { + "text": { + "field": "Temp (°C)", + "type": "quantitative", + "format": ".0f" + }, + "color": { + "condition": { + "test": "datum['Temp (°C)'] > 22 || datum['Temp (°C)'] <= -6", + "value": "#ffffff" + }, + "value": "#121212" + } + } + } + ] +} diff --git a/site/src/playground/theme-lab-assets/temp-heatmap.powerbi.json b/site/src/playground/theme-lab-assets/temp-heatmap.powerbi.json new file mode 100644 index 00000000..d602d831 --- /dev/null +++ b/site/src/playground/theme-lab-assets/temp-heatmap.powerbi.json @@ -0,0 +1,165 @@ +{ + "$schema": "https://vega-lite.github.io/schema/vega-lite/v5.json", + "__theme__": "powerbi", + "__design__": [ + "Whole spec re-based onto a dark card surface (#1B1A19). Every ink value had to be re-derived — labels #C8C6C4, title #F3F2F1, hairlines #3B3A39 — because Flint's defaults assume white paper.", + "Colour ramp replaced by an explicit diverging scale pinned at domainMid 0, so 'below freezing' is a readable state and not just a darker blue.", + "Legend moved to the bottom as a horizontal gradient with a fixed 90px length — dashboard tiles need the legend in the same place on every visual, at a size that does not reflow when the data changes.", + "Data labels layered on each cell with a conditional colour (dark ink on light cells, light ink on dark cells) — the standard product answer to 'I need the number, not just the shade'.", + "Category axes stripped of domain, ticks and titles; month labels shortened to 10pt Segoe UI so 12 columns fit in a tile without rotating.", + "Cell gaps introduced via a 1px stroke in the surface colour, which reads as a grid on dark backgrounds where gridlines would not.", + "Explicit month sort added — the default nominal ordering is data-order, which is stable here but not guaranteed after a refresh." + ], + "background": "#1b1a19", + "padding": { "left": 10, "top": 8, "right": 12, "bottom": 8 }, + "width": 288, + "height": 130, + "title": { + "text": "Average monthly temperature", + "subtitle": ["°C, climate normals, four cities"] + }, + "data": { + "values": [ + { "City": "Singapore", "Month": "Jan", "Temp (°C)": 26 }, + { "City": "Singapore", "Month": "Feb", "Temp (°C)": 27 }, + { "City": "Singapore", "Month": "Mar", "Temp (°C)": 28 }, + { "City": "Singapore", "Month": "Apr", "Temp (°C)": 28 }, + { "City": "Singapore", "Month": "May", "Temp (°C)": 28 }, + { "City": "Singapore", "Month": "Jun", "Temp (°C)": 28 }, + { "City": "Singapore", "Month": "Jul", "Temp (°C)": 27 }, + { "City": "Singapore", "Month": "Aug", "Temp (°C)": 27 }, + { "City": "Singapore", "Month": "Sep", "Temp (°C)": 27 }, + { "City": "Singapore", "Month": "Oct", "Temp (°C)": 27 }, + { "City": "Singapore", "Month": "Nov", "Temp (°C)": 26 }, + { "City": "Singapore", "Month": "Dec", "Temp (°C)": 26 }, + { "City": "Cairo", "Month": "Jan", "Temp (°C)": 14 }, + { "City": "Cairo", "Month": "Feb", "Temp (°C)": 15 }, + { "City": "Cairo", "Month": "Mar", "Temp (°C)": 18 }, + { "City": "Cairo", "Month": "Apr", "Temp (°C)": 22 }, + { "City": "Cairo", "Month": "May", "Temp (°C)": 26 }, + { "City": "Cairo", "Month": "Jun", "Temp (°C)": 28 }, + { "City": "Cairo", "Month": "Jul", "Temp (°C)": 29 }, + { "City": "Cairo", "Month": "Aug", "Temp (°C)": 29 }, + { "City": "Cairo", "Month": "Sep", "Temp (°C)": 27 }, + { "City": "Cairo", "Month": "Oct", "Temp (°C)": 24 }, + { "City": "Cairo", "Month": "Nov", "Temp (°C)": 20 }, + { "City": "Cairo", "Month": "Dec", "Temp (°C)": 16 }, + { "City": "Moscow", "Month": "Jan", "Temp (°C)": -9 }, + { "City": "Moscow", "Month": "Feb", "Temp (°C)": -7 }, + { "City": "Moscow", "Month": "Mar", "Temp (°C)": -1 }, + { "City": "Moscow", "Month": "Apr", "Temp (°C)": 7 }, + { "City": "Moscow", "Month": "May", "Temp (°C)": 13 }, + { "City": "Moscow", "Month": "Jun", "Temp (°C)": 17 }, + { "City": "Moscow", "Month": "Jul", "Temp (°C)": 19 }, + { "City": "Moscow", "Month": "Aug", "Temp (°C)": 17 }, + { "City": "Moscow", "Month": "Sep", "Temp (°C)": 11 }, + { "City": "Moscow", "Month": "Oct", "Temp (°C)": 5 }, + { "City": "Moscow", "Month": "Nov", "Temp (°C)": -1 }, + { "City": "Moscow", "Month": "Dec", "Temp (°C)": -6 }, + { "City": "Seattle", "Month": "Jan", "Temp (°C)": 5 }, + { "City": "Seattle", "Month": "Feb", "Temp (°C)": 6 }, + { "City": "Seattle", "Month": "Mar", "Temp (°C)": 8 }, + { "City": "Seattle", "Month": "Apr", "Temp (°C)": 10 }, + { "City": "Seattle", "Month": "May", "Temp (°C)": 13 }, + { "City": "Seattle", "Month": "Jun", "Temp (°C)": 16 }, + { "City": "Seattle", "Month": "Jul", "Temp (°C)": 19 }, + { "City": "Seattle", "Month": "Aug", "Temp (°C)": 19 }, + { "City": "Seattle", "Month": "Sep", "Temp (°C)": 16 }, + { "City": "Seattle", "Month": "Oct", "Temp (°C)": 11 }, + { "City": "Seattle", "Month": "Nov", "Temp (°C)": 7 }, + { "City": "Seattle", "Month": "Dec", "Temp (°C)": 4 } + ] + }, + "encoding": { + "x": { + "field": "Month", + "type": "nominal", + "title": null, + "sort": ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"], + "axis": { "domain": false, "ticks": false, "grid": false, "labelAngle": 0, "labelPadding": 4 } + }, + "y": { + "field": "City", + "type": "nominal", + "title": null, + "sort": ["Singapore", "Cairo", "Moscow", "Seattle"], + "axis": { "domain": false, "ticks": false, "grid": false, "labelPadding": 6 } + } + }, + "layer": [ + { + "mark": { "type": "rect", "stroke": "#1b1a19", "strokeWidth": 1 }, + "encoding": { + "color": { + "field": "Temp (°C)", + "type": "quantitative", + "title": "°C", + "scale": { + "domainMid": 0, + "range": ["#118dff", "#5aa9f0", "#4a4948", "#e08a4a", "#d64550"] + }, + "legend": { + "orient": "bottom", + "direction": "horizontal", + "gradientLength": 90, + "gradientThickness": 8, + "titleOrient": "left", + "offset": 6, + "format": ".0f" + } + } + } + }, + { + "mark": { + "type": "text", + "font": "'Segoe UI', 'Segoe UI Variable', system-ui, sans-serif", + "fontSize": 8.5 + }, + "encoding": { + "text": { "field": "Temp (°C)", "type": "quantitative", "format": ".0f" }, + "color": { + "condition": { "test": "datum['Temp (°C)'] > 4 && datum['Temp (°C)'] < 22", "value": "#f3f2f1" }, + "value": "#12100f" + } + } + } + ], + "config": { + "background": "#1b1a19", + "font": "'Segoe UI', 'Segoe UI Variable', system-ui, -apple-system, sans-serif", + "title": { + "font": "'Segoe UI', 'Segoe UI Variable', system-ui, sans-serif", + "fontSize": 12.5, + "fontWeight": 600, + "color": "#f3f2f1", + "anchor": "start", + "offset": 10, + "subtitleFont": "'Segoe UI', 'Segoe UI Variable', system-ui, sans-serif", + "subtitleFontSize": 10, + "subtitleColor": "#a19f9d", + "subtitlePadding": 5 + }, + "view": { "stroke": null }, + "axis": { + "labelFont": "'Segoe UI', 'Segoe UI Variable', system-ui, sans-serif", + "labelFontSize": 9.5, + "labelColor": "#c8c6c4", + "titleFont": "'Segoe UI', 'Segoe UI Variable', system-ui, sans-serif", + "titleFontSize": 9.5, + "titleColor": "#a19f9d", + "domain": false, + "ticks": false, + "grid": false + }, + "legend": { + "labelFont": "'Segoe UI', 'Segoe UI Variable', system-ui, sans-serif", + "labelFontSize": 9, + "labelColor": "#c8c6c4", + "titleFont": "'Segoe UI', 'Segoe UI Variable', system-ui, sans-serif", + "titleFontSize": 9, + "titleColor": "#a19f9d", + "titleFontWeight": 400 + } + } +} diff --git a/site/src/playground/theme-lab-assets/temp-uncertainty.flint.json b/site/src/playground/theme-lab-assets/temp-uncertainty.flint.json new file mode 100644 index 00000000..3859d08d --- /dev/null +++ b/site/src/playground/theme-lab-assets/temp-uncertainty.flint.json @@ -0,0 +1,121 @@ +{ + "mark": { + "type": "area", + "opacity": 0.5, + "line": { + "strokeWidth": 1 + } + }, + "encoding": { + "x": { + "field": "Year", + "type": "temporal" + }, + "y": { + "field": "Lower (°C)", + "type": "quantitative", + "scale": { + "zero": false, + "nice": true + }, + "axis": { + "format": ",.12~g" + } + }, + "y2": { + "field": "Upper (°C)" + } + }, + "config": { + "view": { + "continuousWidth": 308, + "continuousHeight": 254 + }, + "axisX": { + "labelLimit": 100, + "labelFontSize": 10, + "titleFontSize": 11 + }, + "axisY": { + "labelFontSize": 10, + "titleFontSize": 11 + }, + "legend": { + "labelFontSize": 10, + "titleFontSize": 11 + }, + "facet": { + "spacing": 25 + } + }, + "data": { + "values": [ + { + "Year": "1850", + "Anomaly (°C)": -0.42, + "Lower (°C)": -0.62, + "Upper (°C)": -0.22 + }, + { + "Year": "1870", + "Anomaly (°C)": -0.36, + "Lower (°C)": -0.53, + "Upper (°C)": -0.19 + }, + { + "Year": "1890", + "Anomaly (°C)": -0.42, + "Lower (°C)": -0.56, + "Upper (°C)": -0.28 + }, + { + "Year": "1910", + "Anomaly (°C)": -0.44, + "Lower (°C)": -0.55, + "Upper (°C)": -0.33 + }, + { + "Year": "1930", + "Anomaly (°C)": -0.16, + "Lower (°C)": -0.25, + "Upper (°C)": -0.07 + }, + { + "Year": "1950", + "Anomaly (°C)": -0.17, + "Lower (°C)": -0.24, + "Upper (°C)": -0.1 + }, + { + "Year": "1970", + "Anomaly (°C)": -0.08, + "Lower (°C)": -0.14, + "Upper (°C)": -0.02 + }, + { + "Year": "1990", + "Anomaly (°C)": 0.25, + "Lower (°C)": 0.2, + "Upper (°C)": 0.3 + }, + { + "Year": "2010", + "Anomaly (°C)": 0.56, + "Lower (°C)": 0.52, + "Upper (°C)": 0.6 + }, + { + "Year": "2023", + "Anomaly (°C)": 1.11, + "Lower (°C)": 1.07, + "Upper (°C)": 1.15 + } + ] + }, + "title": { + "text": "The record gets more certain as it gets warmer", + "subtitle": [ + "Global mean temperature anomaly against 1961–1990, with 95% confidence interval, °C" + ] + } +} diff --git a/site/src/playground/theme-lab-assets/temp-uncertainty.nature.json b/site/src/playground/theme-lab-assets/temp-uncertainty.nature.json new file mode 100644 index 00000000..9b9abb34 --- /dev/null +++ b/site/src/playground/theme-lab-assets/temp-uncertainty.nature.json @@ -0,0 +1,100 @@ +{ + "$schema": "https://vega-lite.github.io/schema/vega-lite/v5.json", + "__theme__": "nature", + "__design__": [ + "The central estimate drawn inside its own interval. Flint's Range Area template has channels for y and y2 and none for the value the interval is about, so the anomaly — the number every sentence about this dataset quotes — is in the table and not on the chart. A confidence band with no estimate in it states the uncertainty and withholds the result.", + "The band's edges unstroked. Flint outlines the area, which draws a 95% interval as though it had a hard boundary at exactly 1.96σ; the whole content of the interval is that its edge is soft. Fill only, no line.", + "Axis title rewritten from 'Lower (°C), Upper (°C)' to the quantity being measured. Flint concatenates the names of the two encoded fields, which names the geometry rather than the variable and implies the panel shows two different things.", + "Zero pinned as a labelled tick. The series is an anomaly against the 1961–1990 mean, so zero is not an arbitrary round number — it is the definition of the baseline, and a reader cannot interpret a value of −0.42 without it.", + "Okabe–Ito blue at two weights: the band at 25% and the estimate line at full strength. The pair survives greyscale conversion and every form of colour blindness, because the distinction carried is lightness, not hue.", + "Column-width geometry — 89 mm at 8.5pt, hairline L-spines, outward ticks, no gridlines, and units in the axis title as the caption cannot supply them." + ], + "background": "#ffffff", + "padding": { "left": 4, "top": 4, "right": 8, "bottom": 4 }, + "width": 240, + "height": 160, + "title": { + "text": "The record gets more certain as it gets warmer", + "subtitle": ["Global mean temperature anomaly against 1961–1990, with 95% confidence interval, °C"] + }, + "data": { + "values": [ + { "Year": "1850", "Anomaly (°C)": -0.42, "Lower (°C)": -0.62, "Upper (°C)": -0.22 }, + { "Year": "1870", "Anomaly (°C)": -0.36, "Lower (°C)": -0.53, "Upper (°C)": -0.19 }, + { "Year": "1890", "Anomaly (°C)": -0.42, "Lower (°C)": -0.56, "Upper (°C)": -0.28 }, + { "Year": "1910", "Anomaly (°C)": -0.44, "Lower (°C)": -0.55, "Upper (°C)": -0.33 }, + { "Year": "1930", "Anomaly (°C)": -0.16, "Lower (°C)": -0.25, "Upper (°C)": -0.07 }, + { "Year": "1950", "Anomaly (°C)": -0.17, "Lower (°C)": -0.24, "Upper (°C)": -0.1 }, + { "Year": "1970", "Anomaly (°C)": -0.08, "Lower (°C)": -0.14, "Upper (°C)": -0.02 }, + { "Year": "1990", "Anomaly (°C)": 0.25, "Lower (°C)": 0.2, "Upper (°C)": 0.3 }, + { "Year": "2010", "Anomaly (°C)": 0.56, "Lower (°C)": 0.52, "Upper (°C)": 0.6 }, + { "Year": "2023", "Anomaly (°C)": 1.11, "Lower (°C)": 1.07, "Upper (°C)": 1.15 } + ] + }, + "encoding": { + "x": { + "field": "Year", + "type": "temporal", + "title": "Year", + "axis": { "format": "%Y", "tickCount": 6, "labelFlush": true } + } + }, + "layer": [ + { + "mark": { "type": "area", "color": "#0072b2", "opacity": 0.25, "line": false }, + "encoding": { + "y": { + "field": "Lower (°C)", + "type": "quantitative", + "title": "Temperature anomaly (°C)", + "scale": { "zero": false, "nice": false, "domain": [-0.8, 1.3] }, + "axis": { "values": [-0.5, 0, 0.5, 1], "format": ".1f" } + }, + "y2": { "field": "Upper (°C)" } + } + }, + { + "mark": { "type": "line", "color": "#0072b2", "strokeWidth": 1.3 }, + "encoding": { + "y": { "field": "Anomaly (°C)", "type": "quantitative" } + } + } + ], + "config": { + "background": "#ffffff", + "font": "Arial, Helvetica, sans-serif", + "title": { + "font": "Arial, Helvetica, sans-serif", + "fontSize": 12, + "fontWeight": 700, + "color": "#000000", + "anchor": "start", + "offset": 6, + "subtitleFont": "Arial, Helvetica, sans-serif", + "subtitleFontSize": 8, + "subtitleFontStyle": "italic", + "subtitleColor": "#3c3c3c", + "subtitlePadding": 5 + }, + "view": { "stroke": null }, + "axis": { + "labelFont": "Arial, Helvetica, sans-serif", + "labelFontSize": 8.5, + "labelColor": "#000000", + "labelPadding": 2, + "titleFont": "Arial, Helvetica, sans-serif", + "titleFontSize": 9, + "titleFontWeight": 400, + "titleColor": "#000000", + "titlePadding": 4, + "grid": false, + "domain": true, + "domainColor": "#000000", + "domainWidth": 1, + "ticks": true, + "tickColor": "#000000", + "tickWidth": 1, + "tickSize": 3.5 + } + } +} diff --git a/site/src/playground/theme-lab-assets/titanic.flint.json b/site/src/playground/theme-lab-assets/titanic.flint.json new file mode 100644 index 00000000..4623e08d --- /dev/null +++ b/site/src/playground/theme-lab-assets/titanic.flint.json @@ -0,0 +1,99 @@ +{ + "mark": "bar", + "encoding": { + "x": { + "field": "Class", + "type": "nominal", + "sort": null + }, + "y": { + "field": "Survival (%)", + "type": "quantitative", + "scale": { + "zero": true + }, + "axis": { + "format": ",.12~g" + } + }, + "color": { + "field": "Sex", + "type": "nominal", + "sort": null, + "scale": { + "scheme": "tableau10" + } + }, + "xOffset": { + "field": "Sex", + "type": "nominal", + "sort": null + } + }, + "config": { + "view": { + "continuousWidth": 340, + "continuousHeight": 230 + }, + "axisX": { + "labelLimit": 100, + "labelFontSize": 10, + "titleFontSize": 11, + "labelAngle": 0, + "labelAlign": "center", + "labelBaseline": "top" + }, + "axisY": { + "labelFontSize": 10, + "titleFontSize": 11 + }, + "legend": { + "labelFontSize": 10, + "titleFontSize": 11 + }, + "facet": { + "spacing": 23 + } + }, + "width": { + "step": 46, + "for": "position" + }, + "data": { + "values": [ + { + "Class": "1st", + "Sex": "Female", + "Survival (%)": 97 + }, + { + "Class": "1st", + "Sex": "Male", + "Survival (%)": 34 + }, + { + "Class": "2nd", + "Sex": "Female", + "Survival (%)": 89 + }, + { + "Class": "2nd", + "Sex": "Male", + "Survival (%)": 15 + }, + { + "Class": "3rd", + "Sex": "Female", + "Survival (%)": 49 + }, + { + "Class": "3rd", + "Sex": "Male", + "Survival (%)": 15 + } + ] + }, + "title": { + "text": "Titanic survival rate by class and sex" + } +} diff --git a/site/src/playground/theme-lab-assets/trust-likert.datawrapper.json b/site/src/playground/theme-lab-assets/trust-likert.datawrapper.json new file mode 100644 index 00000000..336164f5 --- /dev/null +++ b/site/src/playground/theme-lab-assets/trust-likert.datawrapper.json @@ -0,0 +1,149 @@ +{ + "$schema": "https://vega-lite.github.io/schema/vega-lite/v5.json", + "__theme__": "datawrapper", + "__design__": [ + "Response scale re-coloured as a sequential ramp from confident blue to muted red (#18A1CD → #9FCBDD → #E0B48C → #C04A4A) instead of tableau10's four unrelated hues. On a Likert scale the categories are ordered, so the colour has to be ordered too.", + "Legend moved to the top as a single horizontal row of large squares, reading left to right in the same order as the stack — the key and the bar now share one grammar.", + "Value axis deleted. Every row totals 100, so one 0–100 axis is repeated five times and measures nothing the reader is asking about; the numbers move onto the segments instead.", + "Segment labels added from the existing Share field, blanked by a condition wherever the segment is too narrow (\u226412%) to hold two digits, with conditional ink for contrast. The condition has to blank the text rather than filter the rows: dropping rows would restack that layer and slide every label off its segment.", + "A numeric rank derived from the response order drives an explicit order channel shared by both layers. The bar layer would sort correctly from its colour domain alone, but the label layer overrides colour to get conditional ink, which leaves it stacking by ascending value; one shared rank keeps the two layers in register.", + "11pt type floor throughout and institution labels left-aligned in near-black #333 — the chart has to survive being embedded at 320px on a phone.", + "Row height raised to a 30px step and bar height cut to 0.66 of the band, which is what makes five long institution names readable without truncation." + ], + "background": "#ffffff", + "padding": { "left": 8, "top": 6, "right": 14, "bottom": 8 }, + "width": 300, + "title": { + "text": "Confidence in US institutions", + "subtitle": ["% of adults expressing each level of confidence"] + }, + "height": { "step": 30 }, + "data": { + "values": [ + { "Institution": "Scientists", "Response": "A great deal", "Share (%)": 39 }, + { "Institution": "Scientists", "Response": "Some", "Share (%)": 45 }, + { "Institution": "Scientists", "Response": "Not much", "Share (%)": 12 }, + { "Institution": "Scientists", "Response": "None at all", "Share (%)": 4 }, + { "Institution": "The military", "Response": "A great deal", "Share (%)": 32 }, + { "Institution": "The military", "Response": "Some", "Share (%)": 43 }, + { "Institution": "The military", "Response": "Not much", "Share (%)": 18 }, + { "Institution": "The military", "Response": "None at all", "Share (%)": 7 }, + { "Institution": "The police", "Response": "A great deal", "Share (%)": 26 }, + { "Institution": "The police", "Response": "Some", "Share (%)": 44 }, + { "Institution": "The police", "Response": "Not much", "Share (%)": 21 }, + { "Institution": "The police", "Response": "None at all", "Share (%)": 9 }, + { "Institution": "The press", "Response": "A great deal", "Share (%)": 11 }, + { "Institution": "The press", "Response": "Some", "Share (%)": 32 }, + { "Institution": "The press", "Response": "Not much", "Share (%)": 34 }, + { "Institution": "The press", "Response": "None at all", "Share (%)": 23 }, + { "Institution": "Congress", "Response": "A great deal", "Share (%)": 8 }, + { "Institution": "Congress", "Response": "Some", "Share (%)": 30 }, + { "Institution": "Congress", "Response": "Not much", "Share (%)": 38 }, + { "Institution": "Congress", "Response": "None at all", "Share (%)": 24 } + ] + }, + "transform": [ + { + "calculate": "indexof(['A great deal', 'Some', 'Not much', 'None at all'], datum.Response)", + "as": "__rank" + } + ], + "encoding": { + "x": { + "field": "Share (%)", + "type": "quantitative", + "stack": "center", + "title": null, + "axis": null + }, + "y": { + "field": "Institution", + "type": "nominal", + "sort": ["Scientists", "The military", "The police", "The press", "Congress"], + "title": null, + "axis": { "domain": false, "ticks": false, "grid": false, "labelPadding": 6 } + }, + "color": { + "field": "Response", + "type": "nominal", + "title": null, + "sort": ["A great deal", "Some", "Not much", "None at all"], + "scale": { + "domain": ["A great deal", "Some", "Not much", "None at all"], + "range": ["#18a1cd", "#9fcbdd", "#e0b48c", "#c04a4a"] + }, + "legend": { + "orient": "top", + "direction": "horizontal", + "symbolType": "square", + "symbolSize": 110, + "columnPadding": 10, + "offset": 4, + "padding": 0 + } + }, + "order": { "field": "__rank", "type": "quantitative" } + }, + "layer": [ + { "mark": { "type": "bar", "height": { "band": 0.66 } } }, + { + "mark": { + "type": "text", + "align": "center", + "baseline": "middle", + "font": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "fontSize": 10, + "fontWeight": 600 + }, + "encoding": { + "x": { + "field": "Share (%)", + "type": "quantitative", + "stack": "center", + "bandPosition": 0.5 + }, + "text": { + "condition": { + "test": "datum['Share (%)'] > 12", + "field": "Share (%)", + "type": "quantitative", + "format": ".0f" + }, + "value": "" + }, + "color": { + "condition": { "test": "datum.Response === 'Some' || datum.Response === 'Not much'", "value": "#333333" }, + "value": "#ffffff" + } + } + } + ], + "config": { + "background": "#ffffff", + "font": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "title": { + "font": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "fontSize": 15, + "fontWeight": 700, + "color": "#333333", + "anchor": "start", + "offset": 12, + "subtitleFont": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "subtitleFontSize": 11, + "subtitleColor": "#767676", + "subtitlePadding": 8 + }, + "view": { "stroke": null }, + "axis": { + "labelFont": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "labelFontSize": 11, + "labelColor": "#333333" + }, + "legend": { + "labelFont": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "labelFontSize": 11, + "labelColor": "#333333", + "symbolStrokeWidth": 0 + } + } +} diff --git a/site/src/playground/theme-lab-assets/trust-likert.flint.json b/site/src/playground/theme-lab-assets/trust-likert.flint.json new file mode 100644 index 00000000..a9a079d8 --- /dev/null +++ b/site/src/playground/theme-lab-assets/trust-likert.flint.json @@ -0,0 +1,170 @@ +{ + "mark": "bar", + "encoding": { + "x": { + "field": "Share (%)", + "type": "quantitative", + "stack": "center", + "scale": { + "zero": true + }, + "axis": { + "format": ",.12~g" + } + }, + "y": { + "field": "Institution", + "type": "nominal", + "sort": null + }, + "color": { + "field": "Response", + "type": "nominal", + "sort": null, + "scale": { + "scheme": "tableau10", + "domain": [ + "A great deal", + "Some", + "Not much", + "None at all" + ] + } + } + }, + "config": { + "view": { + "continuousWidth": 280, + "continuousHeight": 230 + }, + "axisX": { + "labelLimit": 100, + "labelFontSize": 10, + "titleFontSize": 11 + }, + "axisY": { + "labelFontSize": 10, + "titleFontSize": 11 + }, + "legend": { + "labelFontSize": 10, + "titleFontSize": 11 + }, + "facet": { + "spacing": 23 + } + }, + "height": { + "step": 23 + }, + "data": { + "values": [ + { + "Institution": "Scientists", + "Response": "A great deal", + "Share (%)": 39 + }, + { + "Institution": "Scientists", + "Response": "Some", + "Share (%)": 45 + }, + { + "Institution": "Scientists", + "Response": "Not much", + "Share (%)": 12 + }, + { + "Institution": "Scientists", + "Response": "None at all", + "Share (%)": 4 + }, + { + "Institution": "The military", + "Response": "A great deal", + "Share (%)": 32 + }, + { + "Institution": "The military", + "Response": "Some", + "Share (%)": 43 + }, + { + "Institution": "The military", + "Response": "Not much", + "Share (%)": 18 + }, + { + "Institution": "The military", + "Response": "None at all", + "Share (%)": 7 + }, + { + "Institution": "The police", + "Response": "A great deal", + "Share (%)": 26 + }, + { + "Institution": "The police", + "Response": "Some", + "Share (%)": 44 + }, + { + "Institution": "The police", + "Response": "Not much", + "Share (%)": 21 + }, + { + "Institution": "The police", + "Response": "None at all", + "Share (%)": 9 + }, + { + "Institution": "The press", + "Response": "A great deal", + "Share (%)": 11 + }, + { + "Institution": "The press", + "Response": "Some", + "Share (%)": 32 + }, + { + "Institution": "The press", + "Response": "Not much", + "Share (%)": 34 + }, + { + "Institution": "The press", + "Response": "None at all", + "Share (%)": 23 + }, + { + "Institution": "Congress", + "Response": "A great deal", + "Share (%)": 8 + }, + { + "Institution": "Congress", + "Response": "Some", + "Share (%)": 30 + }, + { + "Institution": "Congress", + "Response": "Not much", + "Share (%)": 38 + }, + { + "Institution": "Congress", + "Response": "None at all", + "Share (%)": 24 + } + ] + }, + "title": { + "text": "Confidence in US institutions", + "subtitle": [ + "% of adults expressing each level of confidence" + ] + } +} diff --git a/site/src/playground/theme-lab-assets/us-pyramid.datawrapper.json b/site/src/playground/theme-lab-assets/us-pyramid.datawrapper.json new file mode 100644 index 00000000..0dbbf7fb --- /dev/null +++ b/site/src/playground/theme-lab-assets/us-pyramid.datawrapper.json @@ -0,0 +1,178 @@ +{ + "$schema": "https://vega-lite.github.io/schema/vega-lite/v5.json", + "__theme__": "datawrapper", + "__design__": [ + "Flint's pyramid is upside down. It passes 'sort: null', so the age bands stay in source order and 0–14 lands at the top with 75+ at the bottom. Every population pyramid ever printed runs youngest at the base; Flint's reads as an inverted one, which is exactly the shape a reader interprets as a collapsing birth rate. The order is pinned here.", + "The two-panel hconcat collapsed into one plot with one axis. Flint mirrors by giving the left panel 'scale: {reverse: true}' and the right panel its own scale, so there are two independent rulers and the reader has to verify they match. Male counts are negated in a transform, and a single axis labels them with abs(), so the two halves are provably on the same scale.", + "'Male' and 'Female' stay what they were in Flint: titles for the two halves of the pyramid. They are set in the series colours and dropped to label weight, but they are headings for a region of the plot, not names attached to marks. They are pinned to the top of the plot area with their own two-row constant source, so they sit where a title sits and do not move if the data changes; the earlier draft derived them by filtering the data to the '75+' band, which made a title masquerade as a mark label.", + "Blue-for-boys, red-for-girls dropped. Datawrapper's #18A1CD and #E2A233 carry the split without importing a convention the data does not contain, and they stay distinguishable in greyscale and to the ~8% of men with a red–green deficiency.", + "Value labels added at every bar end. A pyramid is read for shape, but the 75+ band — 10 million men against 15 million women — is the one fact here that shape alone rounds away.", + "11pt type floor, dashed #DCDCDC vertical gridlines behind the bars, no frame, and a hairline footer row to close the block. The zero line is the same gridline as the others, promoted to near-black by a conditional gridColor rather than drawn as an extra layer.", + "Flint's 'opacity: 0.9' removed; it lightens the bars against the gridlines for no stated reason." + ], + "background": "#ffffff", + "padding": { "left": 8, "top": 6, "right": 14, "bottom": 8 }, + "title": { + "text": "A pyramid that is no longer a pyramid", + "subtitle": ["United States population by age and sex, 2020, millions"] + }, + "spacing": 8, + "vconcat": [ + { + "width": 290, + "height": 190, + "data": { + "values": [ + { "Age": "0–14", "Sex": "Male", "Population": 31 }, + { "Age": "0–14", "Sex": "Female", "Population": 30 }, + { "Age": "15–29", "Sex": "Male", "Population": 34 }, + { "Age": "15–29", "Sex": "Female", "Population": 32 }, + { "Age": "30–44", "Sex": "Male", "Population": 33 }, + { "Age": "30–44", "Sex": "Female", "Population": 33 }, + { "Age": "45–59", "Sex": "Male", "Population": 30 }, + { "Age": "45–59", "Sex": "Female", "Population": 31 }, + { "Age": "60–74", "Sex": "Male", "Population": 26 }, + { "Age": "60–74", "Sex": "Female", "Population": 28 }, + { "Age": "75+", "Sex": "Male", "Population": 10 }, + { "Age": "75+", "Sex": "Female", "Population": 15 } + ] + }, + "transform": [ + { "calculate": "datum.Sex === 'Male' ? -datum.Population : datum.Population", "as": "__signed" } + ], + "encoding": { + "x": { + "field": "__signed", + "type": "quantitative", + "stack": null, + "title": null, + "scale": { "nice": false, "domain": [-40, 40] }, + "axis": { + "values": [-30, -20, -10, 0, 10, 20, 30], + "labelExpr": "abs(datum.value)", + "grid": true, + "gridColor": { + "condition": { "test": "datum.value === 0", "value": "#333333" }, + "value": "#dcdcdc" + }, + "gridDash": { + "condition": { "test": "datum.value === 0", "value": [] }, + "value": [2, 2] + }, + "domain": false, + "ticks": false + } + }, + "y": { + "field": "Age", + "type": "nominal", + "title": null, + "sort": ["75+", "60–74", "45–59", "30–44", "15–29", "0–14"], + "scale": { "paddingInner": 0.3 }, + "axis": { "grid": false, "domain": false, "ticks": false, "labelPadding": 6 } + }, + "color": { + "field": "Sex", + "type": "nominal", + "scale": { "domain": ["Male", "Female"], "range": ["#18a1cd", "#e2a233"] }, + "legend": null + } + }, + "layer": [ + { "mark": { "type": "bar" } }, + { + "transform": [{ "filter": "datum.Sex === 'Male'" }], + "mark": { + "type": "text", + "align": "right", + "baseline": "middle", + "dx": -5, + "fontSize": 10.5 + }, + "encoding": { + "text": { "field": "Population", "type": "quantitative", "format": "d" }, + "color": { "value": "#333333" } + } + }, + { + "transform": [{ "filter": "datum.Sex === 'Female'" }], + "mark": { + "type": "text", + "align": "left", + "baseline": "middle", + "dx": 5, + "fontSize": 10.5 + }, + "encoding": { + "text": { "field": "Population", "type": "quantitative", "format": "d" }, + "color": { "value": "#333333" } + } + }, + { + "data": { + "values": [ + { "__half": "Male", "__halfX": -20 }, + { "__half": "Female", "__halfX": 20 } + ] + }, + "mark": { "type": "text", "baseline": "bottom", "dy": -8, "fontSize": 11, "fontWeight": 600 }, + "encoding": { + "x": { "field": "__halfX", "type": "quantitative" }, + "y": { "value": 0 }, + "text": { "field": "__half", "type": "nominal" }, + "color": { + "field": "__half", + "type": "nominal", + "scale": { "domain": ["Male", "Female"], "range": ["#18a1cd", "#e2a233"] }, + "legend": null + } + } + } + ] + }, + { + "data": { "values": [{}] }, + "mark": { "type": "rect", "fill": "#dcdcdc", "stroke": null }, + "width": 310, + "height": 1, + "view": { "stroke": null } + } + ], + "config": { + "background": "#ffffff", + "font": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "title": { + "font": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "fontSize": 14.5, + "fontWeight": 700, + "color": "#333333", + "anchor": "start", + "offset": 12, + "lineHeight": 18, + "subtitleFont": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "subtitleFontSize": 11.5, + "subtitleColor": "#666666", + "subtitlePadding": 7, + "subtitleLineHeight": 15 + }, + "view": { "stroke": null }, + "axis": { + "labelFont": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "labelFontSize": 11, + "labelColor": "#333333", + "labelPadding": 5, + "titleFont": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "titleFontSize": 10.5, + "titleFontWeight": 400, + "titleColor": "#767676", + "grid": false, + "domain": false, + "ticks": false + }, + "legend": { + "labelFont": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "labelFontSize": 11, + "labelColor": "#333333" + } + } +} diff --git a/site/src/playground/theme-lab-assets/us-pyramid.flint.json b/site/src/playground/theme-lab-assets/us-pyramid.flint.json new file mode 100644 index 00000000..97a73fb6 --- /dev/null +++ b/site/src/playground/theme-lab-assets/us-pyramid.flint.json @@ -0,0 +1,184 @@ +{ + "spacing": 0, + "resolve": { + "scale": { + "y": "shared" + } + }, + "hconcat": [ + { + "mark": "bar", + "encoding": { + "y": { + "field": "Age", + "type": "nominal", + "sort": null + }, + "x": { + "scale": { + "reverse": true, + "domain": [ + 0, + 34 + ] + }, + "stack": null, + "field": "Population", + "type": "quantitative" + }, + "opacity": { + "value": 0.9 + }, + "color": { + "value": "#4e79a7" + } + }, + "transform": [ + { + "filter": { + "field": "Sex", + "equal": "Male" + } + } + ], + "title": "Male", + "width": 209, + "height": 230 + }, + { + "mark": "bar", + "encoding": { + "y": { + "axis": null, + "field": "Age", + "type": "nominal", + "sort": null + }, + "x": { + "stack": null, + "field": "Population", + "type": "quantitative", + "scale": { + "domain": [ + 0, + 34 + ] + } + }, + "opacity": { + "value": 0.9 + }, + "color": { + "value": "#e15759" + } + }, + "transform": [ + { + "filter": { + "field": "Sex", + "equal": "Female" + } + } + ], + "title": "Female", + "width": 209, + "height": 230 + } + ], + "config": { + "view": { + "continuousWidth": 280, + "continuousHeight": 230, + "stroke": null + }, + "axisX": { + "labelLimit": 100, + "labelFontSize": 10, + "titleFontSize": 11 + }, + "axisY": { + "labelFontSize": 10, + "titleFontSize": 11 + }, + "legend": { + "labelFontSize": 10, + "titleFontSize": 11 + }, + "facet": { + "spacing": 23 + } + }, + "height": { + "step": 23 + }, + "data": { + "values": [ + { + "Age": "75+", + "Sex": "Male", + "Population": 10 + }, + { + "Age": "75+", + "Sex": "Female", + "Population": 15 + }, + { + "Age": "60–74", + "Sex": "Male", + "Population": 26 + }, + { + "Age": "60–74", + "Sex": "Female", + "Population": 28 + }, + { + "Age": "45–59", + "Sex": "Male", + "Population": 30 + }, + { + "Age": "45–59", + "Sex": "Female", + "Population": 31 + }, + { + "Age": "30–44", + "Sex": "Male", + "Population": 33 + }, + { + "Age": "30–44", + "Sex": "Female", + "Population": 33 + }, + { + "Age": "15–29", + "Sex": "Male", + "Population": 34 + }, + { + "Age": "15–29", + "Sex": "Female", + "Population": 32 + }, + { + "Age": "0–14", + "Sex": "Male", + "Population": 31 + }, + { + "Age": "0–14", + "Sex": "Female", + "Population": 30 + } + ] + }, + "title": { + "text": "A pyramid that is no longer a pyramid", + "subtitle": [ + "United States population by age and sex, 2020, millions" + ] + } +} diff --git a/site/src/playground/theme-lab-assets/us-unemployment.flint.json b/site/src/playground/theme-lab-assets/us-unemployment.flint.json new file mode 100644 index 00000000..fa621aba --- /dev/null +++ b/site/src/playground/theme-lab-assets/us-unemployment.flint.json @@ -0,0 +1,147 @@ +{ + "mark": "line", + "encoding": { + "x": { + "field": "Year", + "type": "temporal" + }, + "y": { + "field": "Unemployment (%)", + "type": "quantitative", + "scale": { + "zero": true + }, + "axis": { + "format": ",.12~g" + } + } + }, + "config": { + "view": { + "continuousWidth": 326, + "continuousHeight": 240 + }, + "axisX": { + "labelLimit": 100, + "labelFontSize": 10, + "titleFontSize": 11 + }, + "axisY": { + "labelFontSize": 10, + "titleFontSize": 11 + }, + "legend": { + "labelFontSize": 10, + "titleFontSize": 11 + }, + "facet": { + "spacing": 24 + } + }, + "data": { + "values": [ + { + "Year": "2000", + "Unemployment (%)": 4 + }, + { + "Year": "2001", + "Unemployment (%)": 4.7 + }, + { + "Year": "2002", + "Unemployment (%)": 5.8 + }, + { + "Year": "2003", + "Unemployment (%)": 6 + }, + { + "Year": "2004", + "Unemployment (%)": 5.5 + }, + { + "Year": "2005", + "Unemployment (%)": 5.1 + }, + { + "Year": "2006", + "Unemployment (%)": 4.6 + }, + { + "Year": "2007", + "Unemployment (%)": 4.6 + }, + { + "Year": "2008", + "Unemployment (%)": 5.8 + }, + { + "Year": "2009", + "Unemployment (%)": 9.3 + }, + { + "Year": "2010", + "Unemployment (%)": 9.6 + }, + { + "Year": "2011", + "Unemployment (%)": 8.9 + }, + { + "Year": "2012", + "Unemployment (%)": 8.1 + }, + { + "Year": "2013", + "Unemployment (%)": 7.4 + }, + { + "Year": "2014", + "Unemployment (%)": 6.2 + }, + { + "Year": "2015", + "Unemployment (%)": 5.3 + }, + { + "Year": "2016", + "Unemployment (%)": 4.9 + }, + { + "Year": "2017", + "Unemployment (%)": 4.4 + }, + { + "Year": "2018", + "Unemployment (%)": 3.9 + }, + { + "Year": "2019", + "Unemployment (%)": 3.7 + }, + { + "Year": "2020", + "Unemployment (%)": 8.1 + }, + { + "Year": "2021", + "Unemployment (%)": 5.3 + }, + { + "Year": "2022", + "Unemployment (%)": 3.6 + }, + { + "Year": "2023", + "Unemployment (%)": 3.6 + } + ] + }, + "title": { + "text": "American unemployment", + "subtitle": [ + "Annual rate, per cent of the labour force, 2000–2023" + ] + } +} diff --git a/site/src/playground/theme-lab-assets/us-unemployment.nyt.json b/site/src/playground/theme-lab-assets/us-unemployment.nyt.json new file mode 100644 index 00000000..c554c31d --- /dev/null +++ b/site/src/playground/theme-lab-assets/us-unemployment.nyt.json @@ -0,0 +1,166 @@ +{ + "$schema": "https://vega-lite.github.io/schema/vega-lite/v5.json", + "__theme__": "nyt", + "__design__": [ + "Y scale forced through zero. A rate that a reader will compare against 'none' has a real zero, and the truncated default made the 2020 spike look like a doubling of an already-high number rather than a jump from 3.7% to 8.1%.", + "Line taken to 2.4px in near-black with a round cap and join, so the pandemic spike keeps its corner instead of being rounded off by a hairline.", + "The first and last points of the series marked and labelled with their own numbers, taken from the same Unemployment field the line already draws. The endpoints are where a reader starts and stops, and they are the two values a line chart makes hardest to recover — everything between them is read as shape.", + "Axis reduced to a horizontal grid of #E4E4E4 hairlines with no y domain and no ticks; the x axis keeps a solid black baseline, which is the paper's standard time-series frame.", + "Y title flattened to the unit '%' sitting horizontally above the top gridline instead of a rotated 'Unemployment (%)'.", + "Tick labels thinned to four decades plus the final year, and formatted as two-digit years after the first, which is how the paper labels a long series.", + "Georgia for the headline and deck, Helvetica for the numbers; the deck states the unit and period so the axis does not have to." + ], + "background": "#ffffff", + "padding": { "left": 4, "top": 4, "right": 10, "bottom": 6 }, + "width": 320, + "height": 210, + "title": { + "text": "American unemployment", + "subtitle": ["Annual rate, per cent of the labour force, 2000–2023"] + }, + "data": { + "values": [ + { "Year": "2000", "Unemployment (%)": 4.0 }, + { "Year": "2001", "Unemployment (%)": 4.7 }, + { "Year": "2002", "Unemployment (%)": 5.8 }, + { "Year": "2003", "Unemployment (%)": 6.0 }, + { "Year": "2004", "Unemployment (%)": 5.5 }, + { "Year": "2005", "Unemployment (%)": 5.1 }, + { "Year": "2006", "Unemployment (%)": 4.6 }, + { "Year": "2007", "Unemployment (%)": 4.6 }, + { "Year": "2008", "Unemployment (%)": 5.8 }, + { "Year": "2009", "Unemployment (%)": 9.3 }, + { "Year": "2010", "Unemployment (%)": 9.6 }, + { "Year": "2011", "Unemployment (%)": 8.9 }, + { "Year": "2012", "Unemployment (%)": 8.1 }, + { "Year": "2013", "Unemployment (%)": 7.4 }, + { "Year": "2014", "Unemployment (%)": 6.2 }, + { "Year": "2015", "Unemployment (%)": 5.3 }, + { "Year": "2016", "Unemployment (%)": 4.9 }, + { "Year": "2017", "Unemployment (%)": 4.4 }, + { "Year": "2018", "Unemployment (%)": 3.9 }, + { "Year": "2019", "Unemployment (%)": 3.7 }, + { "Year": "2020", "Unemployment (%)": 8.1 }, + { "Year": "2021", "Unemployment (%)": 5.3 }, + { "Year": "2022", "Unemployment (%)": 3.6 }, + { "Year": "2023", "Unemployment (%)": 3.6 } + ] + }, + "encoding": { + "x": { + "field": "Year", + "type": "temporal", + "title": null, + "scale": { "type": "utc" }, + "axis": { + "values": ["2000", "2005", "2010", "2015", "2020", "2023"], + "labelExpr": "utcyear(datum.value) === 2000 ? '2000' : utcFormat(datum.value, '’%y')", + "labelFlush": true, + "grid": false, + "domain": true, + "domainColor": "#000000", + "domainWidth": 1, + "ticks": true, + "tickColor": "#000000", + "tickSize": 4, + "labelPadding": 3 + } + }, + "y": { + "field": "Unemployment (%)", + "type": "quantitative", + "title": "%", + "scale": { "domain": [0, 10], "nice": false }, + "axis": { + "values": [0, 2, 4, 6, 8, 10], + "grid": true, + "gridColor": "#e4e4e4", + "gridWidth": 1, + "domain": false, + "ticks": false, + "labelPadding": 5, + "titleAngle": 0, + "titleAlign": "left", + "titleX": -4, + "titleY": -10, + "titleBaseline": "bottom" + } + } + }, + "transform": [ + { + "window": [ + { "op": "row_number", "as": "__i" }, + { "op": "count", "as": "__n" } + ], + "frame": [null, null], + "sort": [{ "field": "Year", "order": "ascending" }] + } + ], + "layer": [ + { + "mark": { + "type": "line", + "color": "#1a1a1a", + "strokeWidth": 2.4, + "strokeCap": "round", + "strokeJoin": "round" + } + }, + { + "transform": [ + { "filter": "datum.__i === 1 || datum.__i === datum.__n" } + ], + "mark": { + "type": "point", + "filled": true, + "size": 45, + "color": "#1a1a1a" + } + }, + { + "transform": [ + { "filter": "datum.__i === 1 || datum.__i === datum.__n" } + ], + "mark": { + "type": "text", + "align": "center", + "baseline": "bottom", + "dy": -7, + "font": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "fontSize": 10, + "fontWeight": 700, + "color": "#1a1a1a" + }, + "encoding": { + "text": { "field": "Unemployment (%)", "type": "quantitative", "format": ".1f" } + } + } + ], + "config": { + "background": "#ffffff", + "font": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "title": { + "font": "Georgia, 'Times New Roman', serif", + "fontSize": 16, + "fontWeight": 700, + "color": "#121212", + "anchor": "start", + "offset": 12, + "subtitleFont": "Georgia, 'Times New Roman', serif", + "subtitleFontSize": 11, + "subtitleColor": "#666666", + "subtitlePadding": 8 + }, + "view": { "stroke": null }, + "axis": { + "labelFont": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "labelFontSize": 10, + "labelColor": "#666666", + "titleFont": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "titleFontSize": 10, + "titleColor": "#666666", + "titleFontWeight": 400 + } + } +} From 55c138c3c2ca97d6d6608b8e2ee5b64a3dd54e1d Mon Sep 17 00:00:00 2001 From: Chenglong Wang Date: Wed, 29 Jul 2026 18:16:52 -0700 Subject: [PATCH 008/164] theme research --- agent-skills/flint-chart-author/SKILL.md | 66 +- docs/api-reference.md | 4 + packages/flint-js/src/core/compute-layout.ts | 42 + packages/flint-js/src/core/field-semantics.ts | 32 +- packages/flint-js/src/core/index.ts | 14 + packages/flint-js/src/core/semantic-types.ts | 60 +- packages/flint-js/src/core/theme/ground.ts | 1456 ++++++++ packages/flint-js/src/core/theme/index.ts | 8 + packages/flint-js/src/core/theme/presence.ts | 203 ++ packages/flint-js/src/core/theme/presets.ts | 52 + .../src/core/theme/presets/datawrapper.ts | 164 + .../src/core/theme/presets/economist.ts | 200 ++ .../src/core/theme/presets/mckinsey.ts | 156 + .../flint-js/src/core/theme/presets/nature.ts | 197 + .../flint-js/src/core/theme/presets/nyt.ts | 178 + .../src/core/theme/presets/powerbi.ts | 187 + packages/flint-js/src/core/theme/types.ts | 680 ++++ packages/flint-js/src/core/types.ts | 32 + packages/flint-js/src/vegalite/assemble.ts | 117 +- .../flint-js/src/vegalite/templates/bar.ts | 28 +- .../flint-js/src/vegalite/templates/bump.ts | 85 +- .../flint-js/src/vegalite/templates/slope.ts | 68 +- .../flint-js/src/vegalite/templates/violin.ts | 167 + packages/flint-js/src/vegalite/theme.ts | 3169 +++++++++++++++++ .../flint-js/tests/heatmap-colors.test.ts | 65 + .../flint-js/tests/theme-legend-rows.test.ts | 90 + packages/flint-js/tests/theme-presets.test.ts | 599 ++++ packages/flint-js/tests/theme-titles.test.ts | 109 + .../assets/flint-chart-author.SKILL.md | 66 +- packages/flint-mcp/src/server.ts | 24 +- packages/flint-mcp/src/tools/list.ts | 21 + packages/flint-mcp/src/tools/schemas.ts | 24 +- packages/flint-mcp/tests/http.test.ts | 1 + packages/flint-mcp/tests/server.test.ts | 1 + site/src/playground/ThemeLab.tsx | 244 +- .../theme-lab-assets/_flint-index.json | 2 +- .../theme-lab-assets/_headlines.json | 2 +- .../theme-lab-assets/_themespecs.json | 907 +---- .../theme-lab-assets/anscombe.flint.json | 6 +- .../theme-lab-assets/auto-mpg.flint.json | 12 +- .../theme-lab-assets/big-mac.flint.json | 12 +- .../theme-lab-assets/browser-pie.flint.json | 12 +- .../theme-lab-assets/causes-death.flint.json | 12 +- .../theme-lab-assets/cities-map.flint.json | 6 +- .../theme-lab-assets/co2-lollipop.flint.json | 12 +- .../theme-lab-assets/compiled/_report.json | 1915 ++++++++++ .../compiled/auto-mpg.nature.json | 327 ++ .../compiled/big-mac.economist.json | 301 ++ .../compiled/browser-pie.datawrapper.json | 204 ++ .../compiled/browser-pie.economist.json | 214 ++ .../compiled/browser-pie.mckinsey.json | 218 ++ .../compiled/browser-pie.nature.json | 192 + .../compiled/browser-pie.nyt.json | 196 + .../compiled/browser-pie.powerbi.json | 174 + .../compiled/causes-death.datawrapper.json | 286 ++ .../compiled/causes-death.economist.json | 287 ++ .../compiled/causes-death.mckinsey.json | 303 ++ .../compiled/causes-death.nature.json | 279 ++ .../compiled/causes-death.nyt.json | 272 ++ .../compiled/causes-death.powerbi.json | 266 ++ .../compiled/co2-lollipop.datawrapper.json | 281 ++ .../compiled/driving.nyt.json | 450 +++ .../compiled/earnings-education.mckinsey.json | 357 ++ .../electricity-mix-area.economist.json | 326 ++ .../compiled/ev-share.datawrapper.json | 310 ++ .../compiled/ev-share.economist.json | 334 ++ .../compiled/ev-share.mckinsey.json | 328 ++ .../compiled/ev-share.nature.json | 254 ++ .../compiled/ev-share.nyt.json | 363 ++ .../compiled/ev-share.powerbi.json | 345 ++ .../compiled/exam-ecdf.nature.json | 301 ++ .../compiled/faithful-hist.nature.json | 265 ++ .../compiled/fed-funds-step.powerbi.json | 290 ++ .../compiled/gapminder-bubble.economist.json | 349 ++ .../compiled/gdp-bartable.mckinsey.json | 419 +++ .../compiled/keeling.nyt.json | 277 ++ .../compiled/kpi-sparkline.powerbi.json | 775 ++++ .../compiled/life-expectancy.economist.json | 426 +++ .../compiled/lifeexp-dumbbell.mckinsey.json | 322 ++ .../compiled/oecd-facet-16.powerbi.json | 654 ++++ .../oecd-unemployment-facet.economist.json | 309 ++ .../compiled/olympic-bump.nyt.json | 377 ++ .../compiled/penguins-box.nature.json | 377 ++ .../compiled/penguins-violin.nature.json | 570 +++ .../compiled/penguins.nature.json | 351 ++ .../population-region.datawrapper.json | 356 ++ .../compiled/population-stream.nyt.json | 415 +++ .../population-waterfall.mckinsey.json | 341 ++ .../compiled/population.mckinsey.json | 303 ++ .../compiled/renewable-bullet.powerbi.json | 404 +++ .../compiled/renewable-kpi.powerbi.json | 256 ++ .../compiled/renewables-projection.nyt.json | 269 ++ .../compiled/seattle-range.economist.json | 252 ++ .../compiled/spending-quintile.mckinsey.json | 343 ++ .../compiled/state-jobless.economist.json | 396 ++ .../state-unemployment.datawrapper.json | 625 ++++ .../compiled/stock-candle.powerbi.json | 336 ++ .../compiled/temp-anomaly.nyt.json | 296 ++ .../compiled/temp-heatmap.datawrapper.json | 483 +++ .../compiled/temp-heatmap.economist.json | 470 +++ .../compiled/temp-heatmap.mckinsey.json | 518 +++ .../compiled/temp-heatmap.nature.json | 466 +++ .../compiled/temp-heatmap.nyt.json | 507 +++ .../compiled/temp-heatmap.powerbi.json | 442 +++ .../compiled/temp-uncertainty.nature.json | 225 ++ .../compiled/trust-likert.datawrapper.json | 316 ++ .../compiled/us-pyramid.datawrapper.json | 475 +++ .../compiled/us-unemployment.nyt.json | 313 ++ .../theme-lab-assets/diamonds.flint.json | 6 +- .../theme-lab-assets/driving.flint.json | 12 +- .../earnings-education.flint.json | 12 +- .../electricity-mix-area.flint.json | 12 +- .../electricity-stacked.flint.json | 6 +- .../theme-lab-assets/ev-share.flint.json | 14 +- .../theme-lab-assets/exam-ecdf.flint.json | 12 +- .../faithful-density.flint.json | 6 +- .../theme-lab-assets/faithful-hist.flint.json | 12 +- .../theme-lab-assets/faithful.flint.json | 6 +- .../fed-funds-step.flint.json | 12 +- .../gapminder-bubble.flint.json | 12 +- .../theme-lab-assets/gdp-bartable.flint.json | 12 +- .../theme-lab-assets/happiness.flint.json | 6 +- .../internet-users.flint.json | 6 +- .../theme-lab-assets/iris-strip.flint.json | 6 +- .../theme-lab-assets/keeling.flint.json | 12 +- .../theme-lab-assets/kpi-sparkline.flint.json | 12 +- .../life-expectancy.flint.json | 14 +- .../lifeexp-dumbbell.flint.json | 12 +- .../theme-lab-assets/marathon-wr.flint.json | 6 +- .../medals-grouped.flint.json | 6 +- .../theme-lab-assets/mobile-donut.flint.json | 6 +- .../nutrition-radar.flint.json | 6 +- .../theme-lab-assets/oecd-facet-16.flint.json | 12 +- .../oecd-unemployment-facet.flint.json | 12 +- .../theme-lab-assets/olympic-bump.flint.json | 39 +- .../theme-lab-assets/penguins-box.flint.json | 12 +- .../penguins-violin.flint.json | 12 +- .../theme-lab-assets/penguins.flint.json | 12 +- .../population-region.flint.json | 12 +- .../population-stream.flint.json | 12 +- .../population-waterfall.flint.json | 12 +- .../theme-lab-assets/population.flint.json | 12 +- .../theme-lab-assets/release-gantt.flint.json | 6 +- .../renewable-bullet.flint.json | 12 +- .../theme-lab-assets/renewable-kpi.flint.json | 12 +- .../renewables-projection.flint.json | 12 +- .../theme-lab-assets/seattle-range.flint.json | 12 +- .../theme-lab-assets/seattle-rose.flint.json | 6 +- .../spending-quintile.flint.json | 12 +- .../theme-lab-assets/state-jobless.flint.json | 12 +- .../theme-lab-assets/sunspots.flint.json | 6 +- .../theme-lab-assets/temp-anomaly.flint.json | 12 +- .../theme-lab-assets/temp-heatmap.flint.json | 18 +- .../temp-uncertainty.flint.json | 12 +- .../theme-lab-assets/titanic.flint.json | 6 +- .../theme-lab-assets/trust-likert.flint.json | 12 +- .../theme-lab-assets/us-pyramid.flint.json | 12 +- .../us-unemployment.flint.json | 12 +- 158 files changed, 32518 insertions(+), 1272 deletions(-) create mode 100644 packages/flint-js/src/core/theme/ground.ts create mode 100644 packages/flint-js/src/core/theme/index.ts create mode 100644 packages/flint-js/src/core/theme/presence.ts create mode 100644 packages/flint-js/src/core/theme/presets.ts create mode 100644 packages/flint-js/src/core/theme/presets/datawrapper.ts create mode 100644 packages/flint-js/src/core/theme/presets/economist.ts create mode 100644 packages/flint-js/src/core/theme/presets/mckinsey.ts create mode 100644 packages/flint-js/src/core/theme/presets/nature.ts create mode 100644 packages/flint-js/src/core/theme/presets/nyt.ts create mode 100644 packages/flint-js/src/core/theme/presets/powerbi.ts create mode 100644 packages/flint-js/src/core/theme/types.ts create mode 100644 packages/flint-js/src/vegalite/theme.ts create mode 100644 packages/flint-js/tests/theme-legend-rows.test.ts create mode 100644 packages/flint-js/tests/theme-presets.test.ts create mode 100644 packages/flint-js/tests/theme-titles.test.ts create mode 100644 site/src/playground/theme-lab-assets/compiled/_report.json create mode 100644 site/src/playground/theme-lab-assets/compiled/auto-mpg.nature.json create mode 100644 site/src/playground/theme-lab-assets/compiled/big-mac.economist.json create mode 100644 site/src/playground/theme-lab-assets/compiled/browser-pie.datawrapper.json create mode 100644 site/src/playground/theme-lab-assets/compiled/browser-pie.economist.json create mode 100644 site/src/playground/theme-lab-assets/compiled/browser-pie.mckinsey.json create mode 100644 site/src/playground/theme-lab-assets/compiled/browser-pie.nature.json create mode 100644 site/src/playground/theme-lab-assets/compiled/browser-pie.nyt.json create mode 100644 site/src/playground/theme-lab-assets/compiled/browser-pie.powerbi.json create mode 100644 site/src/playground/theme-lab-assets/compiled/causes-death.datawrapper.json create mode 100644 site/src/playground/theme-lab-assets/compiled/causes-death.economist.json create mode 100644 site/src/playground/theme-lab-assets/compiled/causes-death.mckinsey.json create mode 100644 site/src/playground/theme-lab-assets/compiled/causes-death.nature.json create mode 100644 site/src/playground/theme-lab-assets/compiled/causes-death.nyt.json create mode 100644 site/src/playground/theme-lab-assets/compiled/causes-death.powerbi.json create mode 100644 site/src/playground/theme-lab-assets/compiled/co2-lollipop.datawrapper.json create mode 100644 site/src/playground/theme-lab-assets/compiled/driving.nyt.json create mode 100644 site/src/playground/theme-lab-assets/compiled/earnings-education.mckinsey.json create mode 100644 site/src/playground/theme-lab-assets/compiled/electricity-mix-area.economist.json create mode 100644 site/src/playground/theme-lab-assets/compiled/ev-share.datawrapper.json create mode 100644 site/src/playground/theme-lab-assets/compiled/ev-share.economist.json create mode 100644 site/src/playground/theme-lab-assets/compiled/ev-share.mckinsey.json create mode 100644 site/src/playground/theme-lab-assets/compiled/ev-share.nature.json create mode 100644 site/src/playground/theme-lab-assets/compiled/ev-share.nyt.json create mode 100644 site/src/playground/theme-lab-assets/compiled/ev-share.powerbi.json create mode 100644 site/src/playground/theme-lab-assets/compiled/exam-ecdf.nature.json create mode 100644 site/src/playground/theme-lab-assets/compiled/faithful-hist.nature.json create mode 100644 site/src/playground/theme-lab-assets/compiled/fed-funds-step.powerbi.json create mode 100644 site/src/playground/theme-lab-assets/compiled/gapminder-bubble.economist.json create mode 100644 site/src/playground/theme-lab-assets/compiled/gdp-bartable.mckinsey.json create mode 100644 site/src/playground/theme-lab-assets/compiled/keeling.nyt.json create mode 100644 site/src/playground/theme-lab-assets/compiled/kpi-sparkline.powerbi.json create mode 100644 site/src/playground/theme-lab-assets/compiled/life-expectancy.economist.json create mode 100644 site/src/playground/theme-lab-assets/compiled/lifeexp-dumbbell.mckinsey.json create mode 100644 site/src/playground/theme-lab-assets/compiled/oecd-facet-16.powerbi.json create mode 100644 site/src/playground/theme-lab-assets/compiled/oecd-unemployment-facet.economist.json create mode 100644 site/src/playground/theme-lab-assets/compiled/olympic-bump.nyt.json create mode 100644 site/src/playground/theme-lab-assets/compiled/penguins-box.nature.json create mode 100644 site/src/playground/theme-lab-assets/compiled/penguins-violin.nature.json create mode 100644 site/src/playground/theme-lab-assets/compiled/penguins.nature.json create mode 100644 site/src/playground/theme-lab-assets/compiled/population-region.datawrapper.json create mode 100644 site/src/playground/theme-lab-assets/compiled/population-stream.nyt.json create mode 100644 site/src/playground/theme-lab-assets/compiled/population-waterfall.mckinsey.json create mode 100644 site/src/playground/theme-lab-assets/compiled/population.mckinsey.json create mode 100644 site/src/playground/theme-lab-assets/compiled/renewable-bullet.powerbi.json create mode 100644 site/src/playground/theme-lab-assets/compiled/renewable-kpi.powerbi.json create mode 100644 site/src/playground/theme-lab-assets/compiled/renewables-projection.nyt.json create mode 100644 site/src/playground/theme-lab-assets/compiled/seattle-range.economist.json create mode 100644 site/src/playground/theme-lab-assets/compiled/spending-quintile.mckinsey.json create mode 100644 site/src/playground/theme-lab-assets/compiled/state-jobless.economist.json create mode 100644 site/src/playground/theme-lab-assets/compiled/state-unemployment.datawrapper.json create mode 100644 site/src/playground/theme-lab-assets/compiled/stock-candle.powerbi.json create mode 100644 site/src/playground/theme-lab-assets/compiled/temp-anomaly.nyt.json create mode 100644 site/src/playground/theme-lab-assets/compiled/temp-heatmap.datawrapper.json create mode 100644 site/src/playground/theme-lab-assets/compiled/temp-heatmap.economist.json create mode 100644 site/src/playground/theme-lab-assets/compiled/temp-heatmap.mckinsey.json create mode 100644 site/src/playground/theme-lab-assets/compiled/temp-heatmap.nature.json create mode 100644 site/src/playground/theme-lab-assets/compiled/temp-heatmap.nyt.json create mode 100644 site/src/playground/theme-lab-assets/compiled/temp-heatmap.powerbi.json create mode 100644 site/src/playground/theme-lab-assets/compiled/temp-uncertainty.nature.json create mode 100644 site/src/playground/theme-lab-assets/compiled/trust-likert.datawrapper.json create mode 100644 site/src/playground/theme-lab-assets/compiled/us-pyramid.datawrapper.json create mode 100644 site/src/playground/theme-lab-assets/compiled/us-unemployment.nyt.json diff --git a/agent-skills/flint-chart-author/SKILL.md b/agent-skills/flint-chart-author/SKILL.md index 52d1adc1..f8817e46 100644 --- a/agent-skills/flint-chart-author/SKILL.md +++ b/agent-skills/flint-chart-author/SKILL.md @@ -83,9 +83,11 @@ published, use the npm package or MCP server for released workflows. interface ChartAssemblyInput { // Bound by the HOST or by you, depending on the situation (see below). data: { values: any[] } | { url: string }; - semantic_types?: Record; // field → semantic type ← you write this + semantic_types?: Record; // field → type ( ← you write this) chart_spec: { // ← you write this chartType: string; // e.g. "Scatter Plot" + title?: string; // the headline — write one + subtitle?: string; // what is measured, of whom, when, in what units encodings: Record; // channel → { field, ... } (or array) baseSize?: { width: number; height: number }; // target layout size, default 400×320 canvasSize?: { width: number; height: number }; // optional hard ceiling on stretch @@ -93,6 +95,7 @@ interface ChartAssemblyInput { }; options?: Record; // global layout options (rarely needed) field_display_names?: Record; // field → readable axis/legend title + theme_spec?: string | ThemeSpec; // design language, e.g. "economist" (Vega-Lite only) } ``` @@ -168,6 +171,44 @@ For a Vega-Lite-specific style tweak: This edited Vega-Lite spec is no longer a portable Flint spec. Do not send it to `render_chart`; use `render_chart` only for Flint `ChartAssemblyInput`. +## Write a headline + +Set `chart_spec.title` to the finding, in a sentence, and `chart_spec.subtitle` +to the reading of it — what is measured, of whom, when, in what units: + +``` +title: "A pyramid that is no longer a pyramid" +subtitle: "United States population by age and sex, 2020, millions" +``` + +`Jan`, `Cairo`, `Chrome` name their own kind; `26`, `5,300`, `0.42` do not, and +the headline is where they get named. Leave it out only where the chart is not +read on its own — a sparkline in a cell, a tile under its own caption. Nothing +breaks: with no headline to lean on, the compiler keeps the axis titles instead. + +## Design languages (`theme_spec`) + +Name a house Flint ships and the compiler styles the chart to it: + +```json +{ "chart_spec": { ... }, "theme_spec": "economist" } +``` + +| id | what it is for | +| --- | --- | +| `nyt` | Newsroom graphics: headline states the finding, values on the marks, series named at their ends. | +| `economist` | Print weekly: compact, flat headline over a deck, units repeated down the ruler. | +| `nature` | Journal figure: small panel, axis titles with units, statistics beside the fit. | +| `mckinsey` | Consulting deck: wide bands, every value printed, headline states the takeaway. | +| `datawrapper` | Embedded web chart: narrow column, plain headline and deck, rule under the footer. | +| `powerbi` | Dashboard tile: compact, legend to the right, latest point emphasised. | + +A house governs the visual only — you still choose the fields, the aggregation +and the sort. Where a house depends on something only you can supply, it says +so: call `list_themes` with an `id` for that house's guidance, and read it +*before* writing the chart spec, since it may change how you prepare the data. +Vega-Lite only for now. You can also pass a `ThemeSpec` object of your own. + ## Step 1 — pick `chartType` Use one of the registered names **exactly**. Vega-Lite is the default and @@ -334,6 +375,29 @@ What choosing well gets you (automatically): If you don't know, use `Quantity` for numbers, `Category` for strings, `Date`/`DateTime` for date-shaped values. Do **not** invent type names. +### Saying more than the type name + +A field's entry can be an object instead of a string when the type alone +understates what you know: + +```json +"semantic_types": { + "anomaly": { "semanticType": "Quantity", "unit": "°C", "divergingMidpoint": 0 }, + "rating": { "semanticType": "Score", "intrinsicDomain": [1, 5] } +} +``` + +- `unit` — the unit or currency code: `"USD"`, `"°C"`, `"kg"`. +- `intrinsicDomain` — the field's own bounds, for bounded scales only: `[1, 5]` + for a five-star rating, `[0, 100]` for a percentage score. Not for + open-ended measures. +- `divergingMidpoint` — where the middle colour of a diverging scale sits. + Set it if you can tell what the reader is comparing against; leave it out if + you can't. +- `sortOrder` — the order the categories should appear in, when the order in + the data is not the one you want and it isn't alphabetical either: + `["Low", "Medium", "High"]`. For a handful of categories, not a long list. + ## Chart-level properties (`chartProperties`) `chartProperties` is an optional per-chart tuning map. Set a property only diff --git a/docs/api-reference.md b/docs/api-reference.md index 60f6a78f..c0391951 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -116,6 +116,8 @@ interface ChartAssemblyInput { semantic_types?: Record; chart_spec: { chartType: string; + title?: string; // headline + subtitle?: string; // deck: what is measured, of whom, when, in what units encodings: Record; // string = field shorthand baseSize?: { width: number; height: number }; // target layout size, default 400×320 canvasSize?: { width: number; height: number }; // optional hard ceiling on stretch @@ -159,6 +161,8 @@ legend headers. Keep encodings bound to the original field names: | Field | Description | |-------|-------------| | `chartType` | Template name — must match a backend registry entry (`"Bar Chart"`, `"Heatmap"`, …) | +| `title` | The headline. Write one: `Jan` and `Cairo` name their own kind, `26` and `5,300` do not, and a theme that omits axis titles is delegating that naming to the headline. Vega-Lite only for now; where no headline is given, the compiler puts the axis titles back. | +| `subtitle` | The deck — what is measured, of whom, when, in what units. | | `encodings` | Channel → encoding map | | `baseSize` | **Target** layout size in pixels (default 400×320): the size the chart aims for with typical data. Dense data may stretch past it, up to the ceiling. | | `canvasSize` | **Hard ceiling:** the maximum size the chart may ever reach, including faceted grids. If omitted, the ceiling is `baseSize × options.maxStretch` (default 1.5×). Per-dimension caps are `βx = canvasSize.width / baseSize.width`, `βy = canvasSize.height / baseSize.height` (each ≥ 1). The base is clamped to the ceiling, so a `canvasSize` on its own acts as a fixed box the chart fills and shrinks to fit without overflowing. | diff --git a/packages/flint-js/src/core/compute-layout.ts b/packages/flint-js/src/core/compute-layout.ts index b9c6d3fa..e7f57ee8 100644 --- a/packages/flint-js/src/core/compute-layout.ts +++ b/packages/flint-js/src/core/compute-layout.ts @@ -240,6 +240,19 @@ export function deriveStretchCaps( // Public API: computeLayout // --------------------------------------------------------------------------- +/** + * The size a cell in a grid wants to be, before the room has its say. Larger + * than a bar's band because a tone needs area to be compared: below about + * twenty pixels a patch reads as grout between its neighbours. + */ +const CELL_BAND_SIZE = 28; + +/** + * How much of the wider step squaring a grid may cost. At 1.5 a 30px step will + * give way to a 20px square, but not to a 15px one. + */ +const SQUARE_CELL_TOLERANCE = 1.5; + /** * Phase 1: Compute layout decisions. * @@ -899,6 +912,35 @@ export function computeLayout( else subplotHeight = Math.round(stepSize * (count + 1)); } + // --- Square cells --- + // Two banded axes means the marks are cells, not bars. A bar states its + // value as a length along one axis, so its thickness is free; a cell states + // its value as a tone, and the eye compares tones by area. A grid of + // squares reads as a surface; a grid of thin rectangles reads as stripes, + // and invites a comparison along the long side that the data does not + // support. + // + // So the two steps are pulled to one size. The caps used here are the ones + // the stretch budget already produced, which is what lets a grid spend a + // little extra canvas to come out square. Where the categories are too many + // for that — where squaring would cut the wider step by more than a third — + // the grid stays rectangular: a shape nobody asked for is not worth losing + // that much room over. + if (xTotalNominalCount > 0 && yTotalNominalCount > 0 && !xHasGrouping && !yHasGrouping) { + const capX = Math.floor(maxSubplotW / xTotalNominalCount); + const capY = Math.floor(maxSubplotH / yTotalNominalCount); + const generous = Math.round(CELL_BAND_SIZE * Math.max(1, sizeRatio)); + // The square is the narrower of the two steps — that one already fits — + // grown to the generous size if there is room for it on both axes. + const wanted = Math.max(generous, Math.min(xStepSize, yStepSize)); + const square = Math.min(capX, capY, wanted); + const widest = Math.max(xStepSize, yStepSize); + if (square >= minStepVal && square * SQUARE_CELL_TOLERANCE >= widest) { + xStepSize = square; + yStepSize = square; + } + } + // --- Nominal discrete subplot sizing --- // For nominal discrete axes, one backend (VL) overrides subplotWidth // with step-based sizing (width:{step:N}), so the subplot dimension diff --git a/packages/flint-js/src/core/field-semantics.ts b/packages/flint-js/src/core/field-semantics.ts index 63a041a4..91e8b76c 100644 --- a/packages/flint-js/src/core/field-semantics.ts +++ b/packages/flint-js/src/core/field-semantics.ts @@ -61,6 +61,26 @@ export interface SemanticAnnotation { /** Unit or currency code. E.g., "USD", "°C", "kg" */ unit?: string; + /** + * The value a diverging colour scale should pivot on — what the reader is + * being asked to compare against. + * + * This is a judgement, not a fact about the field, which is why it has to + * be declared rather than inferred. A temperature in °C pivots at 0 if the + * question is whether it freezes, and at something nearer 18 if the + * question is whether a city is comfortable to live in; the numbers are + * identical either way and only the question tells them apart. Declare it + * when the chart is about the comparison — above and below an average, a + * target, a baseline period, a comfort line — and leave it out otherwise, + * in which case the pivot falls back to whatever the type and the data + * make obvious: a sign change at zero, a bounded domain's centre, or no + * pivot at all. + * + * Declaring one also *asserts* the split: the scale diverges even if every + * reading happens to land on one side of it. + */ + divergingMidpoint?: number; + /** Explicit ordinal ordering. E.g., ["Low", "Medium", "High"] */ sortOrder?: string[]; } @@ -112,7 +132,7 @@ export interface DivergingInfo { /** Whether this type is always diverging or only when data spans both sides */ inherent: boolean; /** Source of the midpoint determination */ - source: 'unit' | 'type-intrinsic' | 'domain' | 'data'; + source: 'annotation' | 'unit' | 'type-intrinsic' | 'domain' | 'data'; } /** @@ -211,7 +231,7 @@ export function normalizeAnnotation( // ============================================================================= /** Map currency codes to display symbols */ -const CURRENCY_MAP: Record = { +export const CURRENCY_MAP: Record = { USD: '$', EUR: '€', GBP: '£', JPY: '¥', CNY: '¥', KRW: '₩', INR: '₹', BRL: 'R$', CAD: 'CA$', AUD: 'A$', CHF: 'CHF', SEK: 'kr', NOK: 'kr', DKK: 'kr', @@ -854,6 +874,7 @@ export function resolveNice( * Resolve diverging midpoint information for a field. * * Priority chain: + * 0. annotation.divergingMidpoint — the author said what to compare against * 1. annotation.unit → type lookup (°C → 0, °F → 32) * 2. type-intrinsic midpoint (Sentiment → 0, Correlation → 0) * 3. annotation.intrinsicDomain midpoint (Rating [1,5] → 3) @@ -869,6 +890,13 @@ export function resolveDivergingInfo( const entry = getRegistryEntry(semanticType); // Types with diverging='none' don't get diverging treatment + // 0. Declared. Nothing below this line can know what the chart is asking, + // so a stated pivot outranks every inferred one — and it holds even when + // the data sits entirely on one side, because the comparison is the point. + if (annotation.divergingMidpoint !== undefined) { + return { midpoint: annotation.divergingMidpoint, inherent: true, source: 'annotation' }; + } + // 1. Unit-derived (Temperature) if (semanticType === 'Temperature' && annotation.unit) { const unitMidpoints: Record = { diff --git a/packages/flint-js/src/core/index.ts b/packages/flint-js/src/core/index.ts index 14524256..772d32ef 100644 --- a/packages/flint-js/src/core/index.ts +++ b/packages/flint-js/src/core/index.ts @@ -194,3 +194,17 @@ export { resolveStackable, resolveSortDirection, } from './field-semantics'; + +// ThemeSpec (experimental): level 1 vocabulary + level 2 grounding +export { + type ThemeSpec, + type ThemePreset, + type DesignDecisions, + type ThemeReport, + type Presence, + type GroundingContext, + groundTheme, + THEME_PRESETS, + listThemePresets, + resolveThemeSpec, +} from './theme'; diff --git a/packages/flint-js/src/core/semantic-types.ts b/packages/flint-js/src/core/semantic-types.ts index 1039972f..10aa9591 100644 --- a/packages/flint-js/src/core/semantic-types.ts +++ b/packages/flint-js/src/core/semantic-types.ts @@ -642,6 +642,7 @@ const colorSchemes = { // Diverging - good for data with meaningful center point diverging: { redBlue: 'redblue', + blueOrange: 'blueorange', redGrey: 'redgrey', redYellowBlue: 'redyellowblue', redYellowGreen: 'redyellowgreen', @@ -652,6 +653,32 @@ const colorSchemes = { }, }; +/** + * Which end of a diverging scale gets the warm arm. + * + * Every diverging scheme Vega names runs from its *first*-named colour at the + * domain minimum to its last at the maximum: `redblue` is red at the bottom + * and blue at the top, `blueorange` the other way round. So choosing a scheme + * is choosing a polarity, and the polarity belongs to the field, not to the + * palette. Only two orders are intrinsic, and they point opposite ways: + * + * - **Intensity.** Warm means *more*. Nobody reads blue as hotter, denser or + * faster. Temperature is the plain case, but so is any measure whose two + * ends are simply less and more of one thing, pivoted at a reference we + * picked. Warm belongs at the top. + * - **Valence.** Red means *loss*. Money below zero, a shrinking share, a + * poor score, a negative sentiment. Here the sign is the reading, and red + * is the side the reader is meant to wince at. Warm belongs at the bottom. + * + * Nothing else is intrinsic. Two named sides — a political lean, agree against + * disagree — carry their colour in the categories, not in the scale. And a + * field with no valence at all still leaves the reader holding "warmer is + * more", which is why intensity, not valence, is what we assume when the type + * tells us nothing. + */ +const DIVERGING_WARM_HIGH = 'blueorange'; +const DIVERGING_WARM_LOW = 'redblue'; + /** * Get recommended color scheme based on semantic type and encoding context. * @@ -707,7 +734,7 @@ export function getRecommendedColorScheme( // Temperature if (semanticType === 'Temperature') { if (colorHint?.type === 'diverging') { - return { scheme: 'redblue', type: 'diverging', reason: 'temperature diverging around freezing point' }; + return { scheme: DIVERGING_WARM_HIGH, type: 'diverging', reason: 'temperature diverging around freezing point, warm end high' }; } return { scheme: 'reds', type: 'sequential', reason: 'temperature single-direction uses sequential' }; } @@ -715,7 +742,7 @@ export function getRecommendedColorScheme( // Percentage if (semanticType === 'Percentage') { if (colorHint?.type === 'diverging') { - return { scheme: 'redblue', type: 'diverging', reason: 'percentage spans positive and negative' }; + return { scheme: DIVERGING_WARM_LOW, type: 'diverging', reason: 'percentage spans positive and negative, red is the losing side' }; } return { scheme: 'oranges', type: 'sequential', reason: 'percentage all same sign uses sequential' }; } @@ -723,7 +750,7 @@ export function getRecommendedColorScheme( // Price/Amount if (['Price', 'Amount'].includes(semanticType)) { if (colorHint?.type === 'diverging') { - return { scheme: 'redblue', type: 'diverging', reason: 'financial data spans positive and negative' }; + return { scheme: DIVERGING_WARM_LOW, type: 'diverging', reason: 'financial data spans positive and negative, red is the losing side' }; } return { scheme: 'goldgreen', type: 'sequential', reason: 'financial data uses gold-green' }; } @@ -731,7 +758,7 @@ export function getRecommendedColorScheme( // Score - evaluation metrics; diverging when hint says so (e.g., domain midpoint) if (semanticType === 'Score') { if (colorHint?.type === 'diverging') { - return { scheme: 'redblue', type: 'diverging', reason: 'score/rating diverging around midpoint' }; + return { scheme: DIVERGING_WARM_LOW, type: 'diverging', reason: 'score diverging around midpoint, red is the poor side' }; } return { scheme: 'yelloworangebrown', type: 'sequential', reason: 'scores use warm sequential' }; } @@ -754,7 +781,12 @@ export function getRecommendedColorScheme( // Geographic locations - use geographic-friendly palettes if (getRegistryEntry(semanticType ?? '').t1 === 'GeoPlace') { if (uniqueValueCount <= 10) { - return { scheme: 'set2', type: 'categorical', reason: 'geographic regions use distinct pastels' }; + // Not a pastel set. A pastel is chosen for filled area — a + // choropleth, a stacked band — where there is enough of it to read + // a faint hue. A place just as often arrives as a one-pixel line + // or as the colour of its own label, and set2's yellow on white is + // then a line the reader has to hunt for. + return { scheme: 'tableau10', type: 'categorical', reason: 'places are named categories, at a contrast that survives thin marks' }; } return { scheme: 'tableau20', type: 'categorical', reason: 'many regions use large categorical' }; } @@ -775,10 +807,10 @@ export function getRecommendedColorScheme( // Names (persons, companies, products) - use saturated schemes for readability if (semanticType === 'Name') { - return { - scheme: uniqueValueCount > 8 ? 'tableau20' : 'set2', - type: 'categorical', - reason: 'names use readable categorical' + return { + scheme: uniqueValueCount > 8 ? 'tableau20' : 'tableau10', + type: 'categorical', + reason: 'names use readable categorical' }; } @@ -792,7 +824,15 @@ export function getRecommendedColorScheme( // PercentageChange) pass through here and should honor their diverging hint. if (measureTypes.has(semanticType)) { if (colorHint?.type === 'diverging') { - return { scheme: 'redblue', type: 'diverging', reason: 'measure with diverging nature' }; + // A signed measure splits into gain and loss, so red goes to the + // bottom. Everything else that happens to straddle a pivot — a + // bare Quantity, a Count, a Distance — splits into less and more, + // and there red at the bottom would tell the reader that small is + // bad when all we meant was small. + const signed = getRegistryEntry(semanticType).t1 === 'SignedMeasure'; + return signed + ? { scheme: DIVERGING_WARM_LOW, type: 'diverging', reason: 'signed measure, red is the negative side' } + : { scheme: DIVERGING_WARM_HIGH, type: 'diverging', reason: 'measure with no valence, warm end high' }; } const sequentialSchemes = ['viridis', 'blues', 'greens', 'reds', 'yelloworangebrown', 'goldgreen']; return { diff --git a/packages/flint-js/src/core/theme/ground.ts b/packages/flint-js/src/core/theme/ground.ts new file mode 100644 index 00000000..c8827ba9 --- /dev/null +++ b/packages/flint-js/src/core/theme/ground.ts @@ -0,0 +1,1456 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** + * Level 2 — grounding. + * + * Takes a portable ThemeSpec and the signals the compiler already resolved for + * *this* chart, and returns `DesignDecisions`: every role bound to a concrete + * part of the chart, every policy resolved against the space actually + * available, and still not a single backend property name. + * + * Grounding is allowed to downgrade. It is not allowed to do so silently. + */ + +import type { + DesignDecisions, + LegendPlacement, + NumericGuard, + Presence, + Ramp, + ResolvedAxis, + ResolvedRule, + ResolvedSeriesInk, + ResolvedText, + SizeToken, + ThemeGuard, + ThemeReport, + ThemeSpec, + TypeRole, +} from './types.js'; +import { + contrastingInk, + isDarkSurface, + luminance, + mixHex, + parseColor, + presenceWidth, + resolvePresenceInk, + sampleRamp, +} from './presence.js'; +import { CURRENCY_MAP } from '../field-semantics.js'; +import { getRegistryEntry } from '../type-registry.js'; + +// --------------------------------------------------------------------------- +// Input +// --------------------------------------------------------------------------- + +/** + * Everything grounding is allowed to look at. Deliberately a flat record of + * compiler *facts* — if grounding needs something that is not here, that is a + * signal the compiler does not actually know it, and the ThemeSpec should not + * have been allowed to depend on it. + */ +export interface GroundingContext { + chartType: string; + /** Template's `markCognitiveChannel`, widened for `angle`/`text` families. */ + markChannel: string; + /** Mark families present in the instantiated chart, e.g. `['bar','text']`. */ + markTypes: string[]; + /** Per-channel resolved semantics (phase 0). */ + channelSemantics: Record; + /** Encoding types after template-driven conversion (e.g. Q→O for bars). */ + resolvedTypes?: Record; + axisFlags?: { x?: { banded?: boolean }; y?: { banded?: boolean } }; + /** + * What the backend spec actually put on x and y. Templates are free to + * name their semantic channels `high`/`low`/`open`/`close`, in which case + * `channelSemantics` says nothing about the axes the reader will see. This + * is a fact about the chart, not a style choice, so grounding may use it. + */ + positional?: { + x?: { type?: string; field?: string }; + y?: { type?: string; field?: string }; + /** Whatever colour-like channel a data mark carries, wherever it sits. */ + color?: { type?: string; field?: string }; + /** Whether the data marks are stacked into segments. */ + stacked?: boolean; + }; + layout: { + subplotWidth: number; + subplotHeight: number; + xStep: number; + yStep: number; + stepPadding: number; + titleFontSize: number; + legendFontSize: number; + facet?: { columns: number; rows: number }; + }; + table: any[]; + canvasSize: { width: number; height: number }; + /** True when the template stacks its series (sum or normalize). */ + stacked?: boolean | 'normalize'; + /** Set when the chart is a share-of-total by construction (pie, donut). */ + partToWhole?: boolean; + /** + * Whether the chart carries a headline. + * + * A house that omits axis titles is not saying the measure needs no name; + * it is saying the name is written above the chart. Where nothing is + * written there, the delegation has nowhere to go. + */ + titled?: boolean; + /** The surface the host page provides, if the theme defers to it. */ + hostSurface?: string; +} + +// --------------------------------------------------------------------------- +// Design tokens +// --------------------------------------------------------------------------- + +/** + * Fold the house's compiler settings under whatever the caller stated. + * + * Three levels, and the order is not negotiable: a value in the chart spec is + * a decision someone made about *this* chart, the theme is a standing + * preference, and flint's default is what is left when nobody said anything. + * + * Returns the merged options; keys the caller left undefined take the house's. + */ +export function resolveCompileDefaults>( + theme: ThemeSpec | undefined, + authored: T | undefined, +): { options: T; report: ThemeReport[] } { + const house = theme?.compileDefaults as Record | undefined; + const stated = (authored ?? {}) as Record; + if (!house) return { options: stated as T, report: [] }; + const merged: Record = { ...stated }; + const report: ThemeReport[] = []; + for (const [key, value] of Object.entries(house)) { + if (value === undefined) continue; + if (stated[key] !== undefined) { + report.push({ + stage: 'ground', + path: `compileDefaults.${key}`, + message: `the house prefers \`${key}: ${JSON.stringify(value)}\`, but the chart states its own — the chart's stands`, + }); + continue; + } + merged[key] = value; + report.push({ + stage: 'ground', + path: `compileDefaults.${key}`, + message: `house preset: \`${key}\` set to ${JSON.stringify(value)}`, + }); + } + return { options: merged as T, report }; +} + +/** + * House rules for a chart type, folded into the chart properties before the + * template runs. + * + * This is the one part of theming that happens *before* the chart is built: + * whether a line carries points or a bump chart is smoothed changes the marks + * themselves, not their dress, so it cannot be done by restyling afterwards. + * + * Only keys the caller left unset are filled, and only keys the template + * actually declares — a house cannot invent a control, and it does not get to + * overrule a reader who has already chosen. + */ +export function resolveChartDefaults( + theme: ThemeSpec | undefined, + chartType: string, + declared: { key: string }[] | undefined, + authored: Record | undefined, + target: Record, +): ThemeReport[] { + const defaults = theme?.chartDefaults; + if (!defaults) return []; + const wanted = { ...(defaults['*'] ?? {}), ...(defaults[chartType] ?? {}) }; + const keys = new Set((declared ?? []).map((p) => p.key)); + const report: ThemeReport[] = []; + for (const [key, value] of Object.entries(wanted)) { + if (!keys.has(key)) { + report.push({ + stage: 'ground', + path: `chartDefaults.${chartType}.${key}`, + message: `the house asks for \`${key}\`, which \`${chartType}\` does not offer — dropped`, + }); + continue; + } + if (authored?.[key] !== undefined) { + report.push({ + stage: 'ground', + path: `chartDefaults.${chartType}.${key}`, + message: `the house prefers \`${key}: ${JSON.stringify(value)}\`, but the chart already states one — the chart's own setting stands`, + }); + continue; + } + target[key] = value; + report.push({ + stage: 'ground', + path: `chartDefaults.${chartType}.${key}`, + message: `house rule: \`${key}\` set to ${JSON.stringify(value)}`, + }); + } + return report; +} + +const TEXT_TOKENS: Record = { + '100': 10, '200': 12, '300': 14, '400': 16, '500': 20, '600': 24, + hero700: 28, hero800: 32, hero900: 40, hero1000: 68, +}; + +const WEIGHTS: Record = { regular: 400, medium: 500, semibold: 600, bold: 700 }; + +function tokenToPx(size: SizeToken | undefined): number | undefined { + if (size == null) return undefined; + if (typeof size === 'number') return size; + const m = /^text\.(.+)$/.exec(size); + if (m && TEXT_TOKENS[m[1]] != null) return TEXT_TOKENS[m[1]]; + const n = Number(size); + return Number.isFinite(n) ? n : undefined; +} + +// --------------------------------------------------------------------------- +// Variant resolution +// --------------------------------------------------------------------------- + +function isPlainObject(v: any): boolean { + return v != null && typeof v === 'object' && !Array.isArray(v); +} + +function deepMerge(base: T, patch: any): T { + if (!isPlainObject(patch)) return (patch === undefined ? base : patch) as T; + const out: any = isPlainObject(base) ? { ...(base as any) } : {}; + for (const k of Object.keys(patch)) { + const pv = (patch as any)[k]; + out[k] = isPlainObject(pv) ? deepMerge(out[k], pv) : pv; + } + return out as T; +} + +function numericGuardHolds(g: NumericGuard, value: number): boolean { + if (g.eq != null && value !== g.eq) return false; + if (g.lt != null && !(value < g.lt)) return false; + if (g.lte != null && !(value <= g.lte)) return false; + if (g.gt != null && !(value > g.gt)) return false; + if (g.gte != null && !(value >= g.gte)) return false; + return true; +} + +interface Signals { + markChannel: string; + hasBandedAxis: boolean; + seriesCount: number; + /** False when a series field exists but this stage cannot count it. */ + seriesCountKnown: boolean; + categoryCount: number; + isPartToWhole: boolean; + isSigned: boolean; + isTemporal: boolean; + isFaceted: boolean; + isSummarised: boolean; + canvasWidth: number; +} + +function guardHolds(guard: ThemeGuard, s: Signals): boolean { + for (const [key, want] of Object.entries(guard)) { + if (want == null) continue; + const got = (s as any)[key]; + if (typeof want === 'object') { + if (!numericGuardHolds(want as NumericGuard, Number(got))) return false; + } else if (got !== want) { + return false; + } + } + return true; +} + +// --------------------------------------------------------------------------- +// Signal derivation +// --------------------------------------------------------------------------- + +const SERIES_CHANNELS = ['color', 'group', 'detail', 'series', 'shape', 'stroke']; +const FACET_CHANNELS = ['column', 'row', 'facet']; + +function distinctCount(table: any[], field: string | undefined): number { + if (!field) return 0; + const seen = new Set(); + for (const row of table) { + const v = row?.[field]; + if (v !== undefined && v !== null) seen.add(v); + } + return seen.size; +} + +function channelType(ctx: GroundingContext, channel: string): string | undefined { + return ctx.resolvedTypes?.[channel] + ?? ctx.channelSemantics?.[channel]?.type + ?? (channel === 'x' || channel === 'y' ? ctx.positional?.[channel]?.type : undefined); +} + +/** Does this channel exist on screen at all, whoever named it? */ +function channelPresent(ctx: GroundingContext, channel: 'x' | 'y'): boolean { + return Boolean(ctx.channelSemantics?.[channel] ?? ctx.positional?.[channel]); +} + +/** + * What a channel actually carries, from whichever stage knows. A layered + * template states its colour field on one layer and nothing at the top, so the + * semantic layer can be silent about a distinction the reader plainly sees. + */ +function channelFact(ctx: GroundingContext, channel: string | undefined): { field?: string; type?: string } | undefined { + if (!channel) return undefined; + const sem = ctx.channelSemantics?.[channel]; + if (sem?.field) return sem; + const pos = (ctx.positional as any)?.[channel]; + return pos?.field ? pos : sem; +} + +/** + * Parts that sum to a hundred are already stated in per cent, whatever the + * field was called. That is a fact about the numbers rather than a guess about + * the name, so grounding may use it. + */ +function percentOfWhole(ctx: GroundingContext, channel: string): string | undefined { + const field = channelFact(ctx, channel)?.field; + if (!field) return undefined; + let sum = 0; + let n = 0; + for (const row of ctx.table) { + const v = row?.[field]; + if (typeof v === 'number') { sum += v; n += 1; } + } + return n >= 3 && Math.abs(sum - 100) < 0.5 ? '%' : undefined; +} + +/** + * The unit a measure is counted in, when the chart already knows it. + * + * Either the annotation says so outright, or the field names it the way a + * person does — `CO₂ (ppm)`, `Unemployment (%)`. Anything longer than a short + * tag is a phrase, not a unit, and belongs in the subtitle. + */ +const UNIT_IN_FIELD_NAME = /\(([^()]{1,6})\)\s*$/; + +function unitText(ctx: GroundingContext, channel: string): string | undefined { + const sem = ctx.channelSemantics?.[channel]; + const declared = sem?.semanticAnnotation?.unit; + const field = sem?.field ?? (ctx.positional as any)?.[channel]?.field; + const named = typeof field === 'string' ? field.match(UNIT_IN_FIELD_NAME) : null; + const raw = (typeof declared === 'string' && declared.length > 0 && declared.length <= 6) + ? declared + : named?.[1]; + if (!raw) return undefined; + // A currency is written with its sign, not its ISO code: `$8`, not `8 USD`. + return CURRENCY_MAP[raw.toUpperCase()] ?? raw; +} + +/** + * Whether the labels on a channel say what they are without being told. + * + * `Jan`, `Cairo`, `Chrome`, `2019` name themselves: a reader who sees them + * knows at once what kind of thing they are, and a title over them — + * `Month`, `City` — only repeats what is already on the page. `26`, `5300`, + * `0.42` do not: a number is an instance of nothing until someone says what it + * counts, and the title is where that is said. + * + * The registry already sorts this out. A type the reader meets as a *name* or + * a *date* carries its own kind; one they meet as a *quantity* does not, and + * neither does a rank or a bin, whose labels are numbers wearing an order. + * This is a fact about the field, so grounding may use it — it is exactly what + * a house means when it says it wants a title only `whenAmbiguous`. + */ +function labelsNameThemselves(ctx: GroundingContext, channel: string | undefined): boolean { + if (!channel) return false; + const semanticType = ctx.channelSemantics?.[channel]?.semanticAnnotation?.semanticType; + if (typeof semanticType !== 'string') { + // Nothing said about the field. Fall back to what the chart put on the + // channel: names and dates read as themselves, numbers do not. + const type = channelType(ctx, channel); + return type === 'nominal' || type === 'temporal'; + } + const entry = getRegistryEntry(semanticType); + return entry.t1 === 'DateGranule' + || entry.visEncodings.includes('nominal') + || entry.visEncodings.includes('temporal'); +} + +/** + * A field that no row carries is not a field with one value — it is a field + * this stage cannot count, usually because a backend transform will create it. + * Saying "one" would silently collapse a colour scale. + */ +function fieldPresent(table: any[], field: string | undefined): boolean { + if (!field) return false; + return table.some((row) => row != null && Object.prototype.hasOwnProperty.call(row, field)); +} + +interface Bindings { + measureChannels: Array<'x' | 'y'>; + categoricalChannel?: 'x' | 'y'; + seriesChannel?: string; + facetChannel?: string; +} + +function bindRoles(ctx: GroundingContext): Bindings { + const measureChannels: Array<'x' | 'y'> = []; + let categoricalChannel: 'x' | 'y' | undefined; + + for (const ch of ['x', 'y'] as const) { + if (!channelPresent(ctx, ch)) continue; + const t = channelType(ctx, ch); + const banded = ctx.axisFlags?.[ch]?.banded === true; + // A banded axis carries identity even when its field is quantitative + // (a binned histogram axis), so banding wins over the encoding type. + if (t === 'quantitative' && !banded) measureChannels.push(ch); + else categoricalChannel = ch; + } + // Both quantitative (scatter): there is no categorical axis, and both axes + // take the measure role. Both discrete (heatmap): neither does. + if (measureChannels.length === 2) categoricalChannel = undefined; + + const seriesChannel = SERIES_CHANNELS.find((c) => ctx.channelSemantics?.[c]?.field) + ?? (ctx.positional?.color?.field ? 'color' : undefined); + const facetChannel = FACET_CHANNELS.find((c) => ctx.channelSemantics?.[c]?.field); + return { measureChannels, categoricalChannel, seriesChannel, facetChannel }; +} + +function deriveSignals(ctx: GroundingContext, b: Bindings): Signals { + const seriesField = channelFact(ctx, b.seriesChannel)?.field; + const catField = channelFact(ctx, b.categoricalChannel)?.field; + + let isSigned = false; + // Not only the measures on the axes: a heat map counts in colour, and a + // temperature that goes below zero is signed wherever it is drawn. + const signedChannels = [...b.measureChannels, ...(b.seriesChannel ? [b.seriesChannel] : [])]; + for (const ch of signedChannels) { + const f = channelFact(ctx, ch)?.field; + if (!f) continue; + for (const row of ctx.table) { + const v = row?.[f]; + if (typeof v === 'number' && v < 0) { isSigned = true; break; } + } + if (isSigned) break; + } + + const isTemporal = (['x', 'y'] as const).some((ch) => channelType(ctx, ch) === 'temporal'); + + const seriesKnown = fieldPresent(ctx.table, seriesField); + + return { + markChannel: ctx.markChannel, + hasBandedAxis: ctx.axisFlags?.x?.banded === true || ctx.axisFlags?.y?.banded === true, + seriesCount: seriesField ? (seriesKnown ? distinctCount(ctx.table, seriesField) : 0) : 1, + seriesCountKnown: !seriesField || seriesKnown, + categoryCount: catField ? distinctCount(ctx.table, catField) : 0, + isPartToWhole: ctx.partToWhole === true + || ctx.stacked === 'normalize' + || Boolean(ctx.channelSemantics?.theta?.field), + isSigned, + isTemporal, + isFaceted: Boolean(b.facetChannel), + isSummarised: ctx.markTypes.some((m) => m === 'boxplot' || m === 'errorbar' || m === 'errorband'), + canvasWidth: Math.round(ctx.layout.subplotWidth || ctx.canvasSize.width), + }; +} + +// --------------------------------------------------------------------------- +// Grounding +// --------------------------------------------------------------------------- + +export function groundTheme(themeIn: ThemeSpec, ctx: GroundingContext): DesignDecisions { + const report: ThemeReport[] = []; + const say = (path: string, message: string) => report.push({ stage: 'ground', path, message }); + + const bindings = bindRoles(ctx); + const signals = deriveSignals(ctx, bindings); + + // --- variants ----------------------------------------------------------- + let theme: ThemeSpec = themeIn; + for (const variant of themeIn.variants ?? []) { + if (!variant.when || !guardHolds(variant.when, signals)) continue; + theme = deepMerge(theme, variant.then); + say('variants', `applied variant ${JSON.stringify(variant.when)}${variant.because ? ` — ${variant.because}` : ''}`); + } + + // --- surface ------------------------------------------------------------ + const houseCanvas = theme.ink.surface?.canvas; + const deferToHost = (theme.ink.surface?.source ?? 'house') === 'host'; + const canvas = (deferToHost ? (ctx.hostSurface ?? houseCanvas) : houseCanvas) ?? '#ffffff'; + const plot = theme.ink.surface?.plot ?? canvas; + const panel = theme.ink.surface?.panel ?? plot; + const dark = isDarkSurface(plot); + + const text = { + primary: theme.ink.text?.primary ?? contrastingInk(plot, '#f3f2f1', '#121212'), + secondary: theme.ink.text?.secondary ?? mixHex(plot, contrastingInk(plot, '#ffffff', '#000000'), 0.72), + muted: theme.ink.text?.muted ?? mixHex(plot, contrastingInk(plot, '#ffffff', '#000000'), 0.45), + inverse: theme.ink.text?.inverse ?? (dark ? '#121212' : '#ffffff'), + }; + const foreground = text.primary; + + // --- typography --------------------------------------------------------- + // Grounding resolves size against the space actually available: the same + // token is a different number of pixels on a 700px chart and a 250px one. + const targetWidth = theme.layout?.targetWidth ?? 300; + const available = ctx.layout.subplotWidth || ctx.canvasSize.width || targetWidth; + const scale = clamp(Math.pow(available / targetWidth, 0.3), 0.85, 1.2); + const minSize = theme.type?.minSize ?? 8; + const bodyFamily = theme.type?.axisLabel?.family + ?? theme.type?.valueLabel?.family + ?? theme.type?.headline?.family; + + function resolveType(role: TypeRole | undefined, fallbackSize: number, fallbackColor: string): ResolvedText { + const px = tokenToPx(role?.size) ?? fallbackSize; + const sized = Math.max(minSize, Math.round(px * scale * 2) / 2); + return { + font: role?.family ?? bodyFamily, + fontSize: sized, + fontWeight: role?.weight ? WEIGHTS[role.weight] : undefined, + fontStyle: role?.style === 'italic' ? 'italic' : undefined, + color: role?.color ?? fallbackColor, + }; + } + + const axisLabelText = resolveType(theme.type?.axisLabel, 10, text.secondary); + const axisTitleText = resolveType(theme.type?.axisTitle, 10, text.secondary); + const headline = resolveType(theme.type?.headline, 14, text.primary); + const deck = resolveType(theme.type?.deck, 11, text.secondary); + const valueLabel = resolveType(theme.type?.valueLabel, 10, text.primary); + const keyLabel = resolveType(theme.type?.keyLabel, 10, text.secondary); + + // --- structure ---------------------------------------------------------- + const structure = theme.structure ?? {}; + const structureInk = theme.ink.structure ?? {}; + + const ink = (presence: Presence | undefined, roleInk: string | undefined, fallback: Presence) => + resolvePresenceInk({ presence, surface: plot, roleInk, foreground, fallback }); + + const rule = (presence: Presence | undefined, roleInk: string | undefined, fallback: Presence, base = 1): ResolvedRule => { + const color = ink(presence, roleInk, fallback); + return { show: color !== null, color: color ?? 'transparent', width: presenceWidth(presence ?? fallback, base) }; + }; + + const gridStyle = structure.grid?.style ?? 'solid'; + const gridDash = gridStyle === 'dashed' ? [3, 3] : gridStyle === 'dotted' ? [1, 3] : undefined; + + const measureGrid: ResolvedRule = { + ...rule(structure.grid?.measure, structureInk.grid, 'quiet'), + dash: gridDash, + }; + const categoryGrid: ResolvedRule = { + ...rule(structure.grid?.category, structureInk.grid, 'omit'), + dash: gridDash, + }; + // Zero is not one gridline among the others: it is where the measure + // changes sign, and on a chart of lengths it is the line every mark is + // measured from. A house that wants it stated says so; the default is to + // let it be an ordinary line. + const zeroRule: ResolvedRule | undefined = structure.grid?.zero + && structure.grid.zero !== 'omit' + ? rule(structure.grid.zero, structureInk.rule ?? structureInk.axis, 'full') + : undefined; + + const frame = rule(structure.frame, structureInk.frame ?? structureInk.axis, 'omit'); + const baseline = rule(structure.baseline, structureInk.axis, 'full'); + + const truncation = theme.labels?.truncation ?? 'ellipsis'; + const labelFlush = theme.labels?.flush === true; + const axisTitlesPolicy = theme.annotation?.axisTitles ?? 'whenAmbiguous'; + + // Which axis the chart is read *along*. Where a category sits on an axis + // that is the answer; where both axes carry quantities — a connected + // scatter, a phase plot — the horizontal one still runs the reading order. + const indexChannel: 'x' | 'y' | undefined = bindings.categoricalChannel + ?? (bindings.measureChannels.includes('x') && bindings.measureChannels.includes('y') ? 'x' : undefined); + + function buildAxis(channel: 'x' | 'y', role: 'categorical' | 'measure'): ResolvedAxis { + const spec = role === 'measure' ? structure.axis?.measure : structure.axis?.categorical; + const opposite = spec?.placement === 'opposite'; + const orient: ResolvedAxis['orient'] = channel === 'x' + ? (opposite ? 'top' : 'bottom') + : (opposite ? 'right' : 'left'); + + // The axis a reader indexes the chart *by* is not always the discrete + // one: a connected scatter has two quantities and still reads left to + // right. Houses that draw a rule under the categories draw it under + // that axis too — it is the base the chart stands on, not a ruler for + // reading values off. So the index axis takes the categorical line + // even when what it carries is a number. + const indexing = role === 'categorical' || channel === indexChannel; + const lineSpec = indexing ? (structure.axis?.categorical ?? spec) : spec; + + // A base rule is the line the marks stand on. Where no axis carries a + // measure at all — a grid of cells, whose quantity is in the colour — + // there is nothing standing on it, and the rule is just a line under a + // list of names. + const standsOnIt = bindings.measureChannels.length > 0; + const domain = indexing && !standsOnIt + ? rule('omit', structureInk.axis, 'omit') + : rule(lineSpec?.line, structureInk.axis, indexing ? 'full' : 'omit'); + if (indexing && !standsOnIt && (lineSpec?.line ?? 'full') !== 'omit') { + say(`structure.axis.categorical.line`, + 'no axis carries a measure — the cells are the structure, and a rule under their names is a line under nothing'); + } + const tickLen = spec?.tickLength === 'long' ? 5 : spec?.tickLength === 'short' ? 2 : 3; + const ticksRule = rule(spec?.ticks, structureInk.axis, 'omit'); + const inward = spec?.tickDirection === 'inward'; + + // A title that only repeats what the labels already say is noise. + // `whenAmbiguous` is a question about the field: `Jan Feb Mar` needs + // nobody to write `Month` over it, and a column of `26 20 14` names + // nothing until someone writes `Temp (°C)` beside it. A rank or a + // binned range is in the second group even though it sits on a + // categorical axis — its labels are numbers wearing an order. + // + // A house that drops its axis titles altogether is leaning on the + // headline to name the measure, and a headline names one. Where both + // rulers carry a measure — one quantity plotted against another — the + // headline cannot say which is which, and two rows of bare numbers + // name nothing. The titles stay. + // + // And a chart with no headline at all has nothing to lean on. `omit` + // is a delegation, not a deletion: where the words it delegates to were + // never written, the labels that name nothing get their title back. + const twoMeasures = bindings.measureChannels.includes('x') + && bindings.measureChannels.includes('y'); + const selfNaming = labelsNameThemselves(ctx, channel); + const undelegated = !selfNaming && ctx.titled !== true; + const showTitle = axisTitlesPolicy === 'always' + ? true + : axisTitlesPolicy === 'omit' + ? ((twoMeasures && role === 'measure') || undelegated) + : !selfNaming; + if (axisTitlesPolicy === 'whenAmbiguous' && selfNaming !== (role !== 'measure')) { + say(`structure.axis.${role}.title`, + selfNaming + ? `the labels on ${channel} name their own kind — a title over them would repeat what is already read` + : `the labels on ${channel} are values, not names — without a title nothing on the axis says what they count`); + } + if (axisTitlesPolicy === 'omit' && undelegated) { + say('annotation.axisTitles', + `the house omits axis titles because the headline names the measure — this chart has no headline, so the title on ${channel} stays`); + } + if (axisTitlesPolicy === 'omit' && twoMeasures && role === 'measure' && channel === 'x') { + say('annotation.axisTitles', + 'both rulers carry a measure — a headline can name one of them, so the axis titles are kept'); + } + + // A measure axis is a ruler, and how finely it is graduated is a house + // matter: roughly one label every 45px reads as ordinary, one every + // 60px as quiet. Three is the floor — two gradations is not a ruler. + const density = spec?.tickDensity; + const span = channel === 'x' ? ctx.layout.subplotWidth : ctx.layout.subplotHeight; + const tickCount = role === 'measure' + ? Math.max(3, Math.round((span / (density === 'sparse' ? 60 : density === 'dense' ? 30 : 45)))) + : undefined; + + // A house that drops axis titles drops the only place the unit was + // written. Where it asks for the unit on the ticks instead, the tag is + // recovered from what the chart already knows the field to be — but + // only where the axis still counts in that unit. A normalized stack + // reads in shares, whatever the field was measured in. + const unitPolicy = theme.annotation?.unit ?? 'never'; + const inFieldUnits = ctx.stacked !== 'normalize' && !ctx.partToWhole; + const unit = role === 'measure' && inFieldUnits ? unitText(ctx, channel) : undefined; + const unitTag = unitPolicy !== 'never' ? unit : undefined; + + // Where the house keeps its axis titles, the title is the natural place + // for the unit — `Weight (lb)` — and the ticks stay bare numbers. + const titleUnit = showTitle && theme.annotation?.unitsInAxisTitle === true ? unit : undefined; + + // The gap between a label and the plot is the same gap whether or not a + // tick is drawn in it. Where there is one, the tick spans the first part + // of that distance and the padding covers the rest; where the house + // draws none — or turns them inward — the padding is the whole distance, + // and 4px leaves the label sitting against the edge of the plot. + const labelGap = labelFlush ? 2 : ticksRule.show && !inward ? 4 : 4 + tickLen; + + return { + role, + orient, + domain, + ticks: { + ...ticksRule, + size: ticksRule.show ? tickLen : 0, + offset: inward ? -tickLen : 0, + }, + grid: indexing ? categoryGrid : measureGrid, + label: { + ...axisLabelText, + limit: truncation === 'never' ? 0 : undefined, + padding: labelGap, + flush: labelFlush, + angle: theme.labels?.angle === 'horizontal' ? 0 : undefined, + }, + title: { + show: showTitle, + ...axisTitleText, + ...(showTitle ? { placement: theme.annotation?.axisTitlePlacement } : {}), + ...(titleUnit ? { unit: titleUnit } : {}), + }, + tickCount, + tickLabels: (indexing ? structure.axis?.categorical : spec)?.tickLabels, + indexing, + ...(indexing || !zeroRule ? {} : { zeroRule }), + unit: unitTag ? { text: unitTag, where: unitPolicy } : undefined, + }; + } + + const axes: DesignDecisions['axes'] = {}; + for (const ch of bindings.measureChannels) axes[ch] = buildAxis(ch, 'measure'); + if (bindings.categoricalChannel) { + axes[bindings.categoricalChannel] = buildAxis(bindings.categoricalChannel, 'categorical'); + } + // A heat map has *two* category axes and the bindings can only name one. + // The other is just as much a ruler for the reader, and left unbound it + // keeps whatever the template drew — its own titles, its own ink. + for (const ch of ['x', 'y'] as const) { + if (!axes[ch] && ctx.positional?.[ch]) axes[ch] = buildAxis(ch, 'categorical'); + } + if (theme.labels?.angle === 'rotated' && axes.x) { + axes.x.label.angle = -45; + } + // A house may set its category labels flat, but "flat" is a preference and + // "legible" is not. Where the band is narrower than the word standing under + // it, the angle goes back to the layout pass, which owns fit. + // + // The reverse is just as true and matters more often: the layout pass sized + // the labels in flint's own type, and a house that sets smaller labels buys + // room the layout did not know it would have. A name that now fits under + // its band should be read straight, not at forty-five degrees because of a + // measurement taken in a font the chart no longer uses. + if (axes.x && (bindings.categoricalChannel === 'x' + || ctx.positional?.x?.type === 'nominal' || ctx.positional?.x?.type === 'ordinal')) { + const field = channelFact(ctx, 'x')?.field ?? ctx.positional?.x?.field; + const type = ctx.positional?.x?.type ?? channelFact(ctx, 'x')?.type; + const banded = type === 'nominal' || type === 'ordinal'; + const longest = field + ? Math.max(0, ...ctx.table.map((r) => String(r?.[field] ?? '').length)) + : 0; + // Mixed-case words average out narrower than the widest glyph — half + // the point size a character is close enough — plus a couple of pixels + // so two names never touch. + const needed = longest * (axes.x.label.fontSize ?? 10) * 0.5 + 2; + const step = ctx.layout.xStep; + const fits = longest > 0 && step > 0 && needed <= step; + if (axes.x.label.angle === 0 && !fits && longest > 0 && step > 0) { + axes.x.label.angle = undefined; + say('axes.x.label.angle', + `the house sets category labels flat, but the widest needs ~${Math.round(needed)}px in a ${Math.round(step)}px band — the angle is left to the layout`); + } else if (axes.x.label.angle == null && fits && banded && theme.labels?.angle !== 'rotated') { + axes.x.label.angle = 0; + say('axes.x.label.angle', + `the widest name needs ~${Math.round(needed)}px and the band is ${Math.round(step)}px — at the house's label size they read straight`); + } + } + + // --- series ink --------------------------------------------------------- + const series = groundSeriesInk(theme, ctx, bindings, signals, say); + + // --- legend ------------------------------------------------------------- + const legendSpec = theme.legend ?? {}; + const ranked: LegendPlacement[] = legendSpec.placement?.length + ? legendSpec.placement + : ['right']; + + const seriesField = channelFact(ctx, bindings.seriesChannel)?.field; + const catField = channelFact(ctx, bindings.categoricalChannel)?.field; + // A legend over a quantitative series is a key to values, not to names. + const seriesIsValueKey = channelFact(ctx, bindings.seriesChannel)?.type === 'quantitative'; + + let legendShow = Boolean(seriesField) + && (!signals.seriesCountKnown || signals.seriesCount > 1) + && legendSpec.show !== 'never'; + if (legendShow && legendSpec.suppressWhenAxisNames && seriesField && seriesField === catField) { + legendShow = false; + say('legend.suppressWhenAxisNames', 'legend removed — it restated the categorical axis'); + } + + // Which placements grounding can offer at all. `inline` needs a label + // anchored to each mark's own geometry, which only line-family charts have + // room for; the ranked list exists precisely so this can fall through. + // + // A stacked band has that room too, and more of it: the name goes *inside* + // the band at its last reading, which is where a reader's eye already is + // when they ask which band this is. + const lineFamily = ctx.markTypes.some((m) => m === 'line' || m === 'trail'); + const bandFamily = ctx.markTypes.some((m) => m === 'area'); + const placementRealizable = (p: LegendPlacement): boolean => { + if (p === 'seriesEnd' || p === 'inline') return (lineFamily || bandFamily) && !signals.isFaceted; + return true; + }; + let placement: LegendPlacement = 'right'; + let fallbacks: LegendPlacement[] = []; + for (const [i, p] of ranked.entries()) { + if (placementRealizable(p)) { + placement = p; + fallbacks = ranked.slice(i + 1).filter(placementRealizable); + break; + } + say('legend.placement', `\`${p}\` not available for this chart — falling through`); + } + + const legendOrient = placement === 'inside' + ? 'top-right' + : placement === 'seriesEnd' || placement === 'inline' + ? 'none' + : (placement as 'top' | 'right' | 'bottom' | 'left'); + + // A key to a set of names needs no title: `Chrome`, `Safari`, `Firefox` + // say what kind of thing they are, and `Browser` written over them repeats + // it. A ramp of numbers says nothing of the sort — `26` is an instance of + // nothing until the key names what it counts. `whenAmbiguous` is that + // question, and it is asked of the field, not answered with a constant. + const titlePolicy = legendSpec.title ?? 'whenAmbiguous'; + const keyNamesItself = labelsNameThemselves(ctx, bindings.seriesChannel); + const legendTitle = titlePolicy === 'always' + || (titlePolicy === 'whenAmbiguous' && legendShow && !keyNamesItself); + if (legendTitle && titlePolicy !== 'always') { + say('legend.title', + 'the key is a ruler, not a list of names — without a title nothing says what its numbers count'); + } + + // --- data labels -------------------------------------------------------- + const dl = theme.dataLabels ?? {}; + const dlPlacement = dl.placement ?? 'outsideMark'; + // A label placed *on* a mark sits on the mark's fill, whatever the house + // said about ink. Contrast is a legibility floor, not a style choice. + const dlInkMode = dl.inkMode + ?? (dlPlacement === 'atMark' ? 'contrastWithMark' : 'fixed'); + if (!dl.inkMode && dlInkMode === 'contrastWithMark') { + say('dataLabels.inkMode', + 'no ink mode declared but the label sits on the mark — it contrasts with what it is printed on'); + } + let dlShow = dl.show === 'always'; + + // A cell in a grid *is* a position. The reader finds it by its row and its + // column, and the number goes in the middle of it — the measure being on + // colour is the reason the number is worth printing, not a reason to + // withhold it. + const gridCells = signals.markChannel === 'color' + && bindings.measureChannels.length === 0 + && Boolean(ctx.positional?.x && ctx.positional?.y); + + // A printed value has to be *keyed* to something the eye can separate — a + // band, a slice, a discrete step. On a continuous-by-continuous plot there + // is no such anchor: every datum would get its own floating number and the + // result is not a labelled chart, it is a chart with numbers spilled on it. + // This gate binds `always` too; `always` is a house habit, not a licence. + // There also has to be a value to print: a heatmap has two banded axes and + // its measure on colour, and a number can only be printed where a measure + // is on a position. Stacked segments have a position but not a readable + // one — a number at the edge of a segment reads as the running total. And + // where the chart summarises a distribution, the marks in a band *are* the + // sample: its subject is the shape, and thirty numbers per band bury it. + const labelable = ((signals.hasBandedAxis && (bindings.measureChannels.length > 0 || gridCells)) + || signals.isPartToWhole) + && !ctx.positional?.stacked + && !signals.isSummarised; + if (dlShow && !labelable) { + dlShow = false; + say('dataLabels.show', ctx.positional?.stacked + ? 'the segments are stacked — a value at a segment edge would read as the running total' + : signals.isSummarised + ? 'the chart summarises a distribution — each band holds a sample, not one quantity to print' + : signals.hasBandedAxis + ? 'the measure is not on an axis — there is no position to print a value at' + : 'no banded axis to key values to — one number per datum would be noise, not a label'); + } + + if (dl.show === 'whenTheyFit') { + const band = signals.hasBandedAxis + ? (bindings.categoricalChannel === 'y' ? ctx.layout.yStep : ctx.layout.xStep) + : Infinity; + const marksOnScreen = Math.max(1, signals.categoryCount || ctx.table.length) * Math.max(1, signals.seriesCount); + dlShow = labelable && band >= (valueLabel.fontSize ?? 10) + 4 && marksOnScreen <= 40; + if (!dlShow) { + say('dataLabels.show', labelable + ? `\`whenTheyFit\` resolved to false (band ${Math.round(band)}px, ${marksOnScreen} marks)` + : '`whenTheyFit` resolved to false — no banded axis to key values to'); + } + } + if (dlShow && legendShow && legendSpec.suppressWhenValuesPrinted) { + // A printed value is not a name. It replaces a legend that was itself + // a value key — a ramp — but never one that carried series names, and + // a banded axis does not help: it names the category, not the series. + if (seriesIsValueKey) { + legendShow = false; + say('legend.suppressWhenValuesPrinted', 'legend removed — the ramp was a value key and every mark now prints its value'); + } else { + say('legend.suppressWhenValuesPrinted', + 'legend kept — the values are printed but nothing else names the series'); + } + } + + // The same argument, one axis over: once every mark states its own value, + // the measure axis is a second copy of the same information. + if (dlShow && structure.axis?.measure?.suppressWhenValuesPrinted) { + for (const ch of bindings.measureChannels) { + const ax = axes[ch]; + if (!ax) continue; + ax.label.show = false; + ax.grid = { show: false, color: 'transparent', width: 0 }; + ax.title = { ...ax.title, show: false }; + ax.ticks = { ...ax.ticks, show: false, size: 0 }; + ax.domain = { ...ax.domain, show: false }; + } + say('structure.axis.measure.suppressWhenValuesPrinted', + 'measure axis removed — every mark prints its own value'); + } + + const measureChannel = bindings.measureChannels[0]; + const numberFormat = groundNumberFormat(theme, ctx, measureChannel); + + // A unit has to be stated somewhere. Normally that is the ruler; a pie has + // no ruler, and a measure axis whose labels were removed is no longer one + // either. Where the house asks for a unit and nothing else can hold it, the + // printed value takes it — whoever printed it, the theme or the template. + const valueUnitChannel = measureChannel + ?? (['theta', 'size', 'radius'] as const).find((ch) => ctx.channelSemantics?.[ch]?.field); + const axisStatesUnit = (['x', 'y'] as const) + .some((ch) => axes[ch]?.unit && axes[ch]!.label.show !== false); + const valueUnit = (theme.annotation?.unit ?? 'never') !== 'never' && !axisStatesUnit + ? (unitText(ctx, valueUnitChannel ?? '') + ?? (signals.isPartToWhole ? percentOfWhole(ctx, valueUnitChannel ?? '') : undefined)) + : undefined; + + // A label placed at the mark sits *inside* it, which only works while the + // mark is longer than the label. Below that length the label has to move + // out, and above the point where the mark reaches the end of the scale an + // outside label has nowhere left to go. Grounding is the stage that can + // say where those two lines are. + let insideMinValue: number | undefined; + let outsideMaxValue: number | undefined; + if (dlShow && measureChannel) { + const field = ctx.channelSemantics[measureChannel]?.field ?? ctx.positional?.[measureChannel]?.field; + const span = measureChannel === 'x' ? ctx.layout.subplotWidth : ctx.layout.subplotHeight; + let maxAbs = 0; + let digits = 1; + if (field) { + for (const row of ctx.table) { + const v = row?.[field]; + if (typeof v !== 'number') continue; + maxAbs = Math.max(maxAbs, Math.abs(v)); + digits = Math.max(digits, String(Math.round(Math.abs(v))).length); + } + } + if (maxAbs > 0 && span > 0) { + const labelPx = (valueLabel.fontSize ?? 10) * 0.62 * digits + 12; + insideMinValue = (labelPx / span) * maxAbs; + outsideMaxValue = maxAbs - insideMinValue; + } + } + + // --- marks -------------------------------------------------------------- + const marksSpec = theme.marks ?? {}; + const separatorInk = marksSpec.separator?.source === 'surface' + ? plot + : structureInk.rule ?? structureInk.grid; + + const separator = marksSpec.separator + ? { + show: (marksSpec.separator.presence ?? 'omit') !== 'omit', + color: (marksSpec.separator.source === 'surface' + ? plot + : ink(marksSpec.separator.presence, separatorInk, 'hairline')) ?? plot, + width: marksSpec.separator.width ?? 1, + } + : undefined; + + // A house that says nothing about its wedges is not silent — it has + // already said how it holds adjoining marks apart, and a pie is adjoining + // marks. Only a house that wants a different answer for the circle than it + // gave for the bars has to say so twice. + const sliceGap = marksSpec.slice?.gap ?? (separator?.show ? separator.width : undefined); + + // And a grid of cells is adjoining marks too. The one thing it does not + // inherit is the ink: a bar's edge may be drawn in structure without + // anyone reading a value into it, but on a grid the fill *is* the value, + // so an edge in any ink but the surface's adds a colour the scale never + // named. A house that wants framed cells asks for them. + const tileGap = marksSpec.tile?.gap ?? (separator?.show ? separator.width : undefined); + + // A dot on a line is drawn over whatever the line passed through on its + // way there, and where several series share a plot that is another + // series' colour. A ring of the page around the dot is what holds the two + // apart: without it a crossing reads as one blob, and the reader cannot + // say which line the dot belongs to. That is a fact about lines meeting, + // not a matter of taste, so a chart that can have crossings gets the ring + // whether or not the house thought to name one — a house that wants none + // says `halo: { presence: 'omit' }`. + const crossingField = ctx.channelSemantics.color?.field + ?? ctx.channelSemantics.detail?.field + ?? ctx.positional?.color?.field; + const linesCanCross = ctx.markTypes.includes('line') + && !!crossingField + && new Set(ctx.table.map((r) => r?.[crossingField!]).filter((v) => v != null)).size > 1; + const haloDeclared = marksSpec.point?.halo?.presence !== undefined; + const halo = haloDeclared + ? marksSpec.point!.halo!.presence !== 'omit' + : linesCanCross; + if (halo && !haloDeclared) { + say('marks.point.halo', + 'the dots carry a ring of the page around them — the lines cross, and at a crossing the ring is the only thing that says which line a dot sits on'); + } + + const marks: DesignDecisions['marks'] = { + bandFraction: marksSpec.bandFraction ?? (1 - (ctx.layout.stepPadding ?? 0.1)), + strokeWidth: marksSpec.strokeWeight ?? 2, + strokeCap: marksSpec.strokeCap, + strokeJoin: marksSpec.strokeJoin, + interpolate: marksSpec.interpolation === 'monotone' + ? 'monotone' + : marksSpec.interpolation === 'step' ? 'step' : undefined, + fillOpacity: marksSpec.fillOpacity, + point: marksSpec.point || halo + ? { + show: (marksSpec.point?.presence ?? 'omit') !== 'omit', + size: marksSpec.point?.size, + // Only a house that spoke about its dots decides how they are + // filled; inventing an answer here would re-fill every + // scatter it has for the sake of a line chart's vertices. + filled: marksSpec.point ? marksSpec.point.fill !== 'hollow' : undefined, + haloColor: halo ? plot : undefined, + haloWidth: marksSpec.point?.halo?.width ?? (halo ? 1.5 : undefined), + } + : undefined, + separator, + slice: sliceGap + ? { + gap: sliceGap, + style: marksSpec.slice?.gapStyle ?? 'rule', + color: separator?.color ?? plot, + } + : undefined, + tile: tileGap + ? { + gap: tileGap, + color: marksSpec.tile?.source === 'structure' + ? (ink('hairline', structureInk.rule, 'hairline') ?? plot) + : plot, + } + : undefined, + connector: marksSpec.connector + ? { + show: (marksSpec.connector.presence ?? 'omit') !== 'omit', + // A connector is not a gridline. It borrows the rule's ink + // where the house declares none of its own, but it is read as + // part of the mark, not through it — so it is scaled at the + // step the house named against whichever of the two inks it + // gave, and a house whose rules are already pale states a + // `connector` ink rather than fading a faint grey further. + color: ink( + marksSpec.connector.presence, + structureInk.connector ?? structureInk.rule, + 'quiet', + ) ?? undefined, + width: marksSpec.connector.weight ?? 1, + // A stem and a bridge are one setting only in the sense that + // both are drawn in structure's ink. What they are worth + // differs: a stem repeats a position already plotted, a bridge + // draws a distance that is plotted nowhere else. So a house + // that says nothing about the bridge is not silent about it + // either — it has already said what a mark of its own weighs. + spanWidth: marksSpec.connector.spanWeight ?? (marksSpec.strokeWeight ?? 2), + ...(marksSpec.connector.style && marksSpec.connector.style !== 'solid' + ? { dash: marksSpec.connector.style === 'dotted' ? [1, 2] : [4, 3] } + : {}), + } + : undefined, + interval: marksSpec.interval + ? { + fillOpacity: marksSpec.interval.fillOpacity, + edge: (marksSpec.interval.edge ?? 'omit') !== 'omit', + } + : undefined, + summary: marksSpec.summary + ? { + fill: (marksSpec.summary.fill ?? 'full') !== 'omit', + outline: (marksSpec.summary.outline ?? 'full') !== 'omit', + centralRule: (marksSpec.summary.centralRule ?? 'full') !== 'omit', + widthFraction: marksSpec.summary.widthFraction, + } + : undefined, + reference: marksSpec.reference + ? { + show: (marksSpec.reference.presence ?? 'omit') !== 'omit', + width: marksSpec.reference.weight ?? 1, + style: marksSpec.reference.style, + label: marksSpec.reference.label === true, + } + : undefined, + zOrder: marksSpec.zOrder ?? 'summaryOverData', + sizeRange: marksSpec.sizeRange, + minSize: marksSpec.minSize, + observations: marksSpec.observations + ? { + expose: marksSpec.observations.expose ?? 'never', + maxRows: marksSpec.observations.maxRows ?? 500, + } + : undefined, + redundantChannels: marksSpec.redundantChannels ?? [], + redundantEncoding: marksSpec.redundantEncoding ?? 'never', + redundant: groundRedundancy(marksSpec, series, signals, say), + }; + + // --- facets ------------------------------------------------------------- + const facetSpec = theme.facets ?? {}; + const headerPresence = facetSpec.header?.presence ?? 'full'; + const facets: DesignDecisions['facets'] = { + header: { + show: headerPresence !== 'omit', + fieldTitle: (facetSpec.header?.fieldTitle ?? 'omit') === 'always', + ...keyLabel, + color: headerPresence === 'emphasised' ? text.primary : keyLabel.color, + }, + panelFrame: (facetSpec.panelFrame ?? 'omit') !== 'omit', + axisRepetition: facetSpec.axisRepetition ?? 'everyPanel', + spacing: facetSpec.spacing === 'compact' ? 8 : facetSpec.spacing === 'airy' ? 24 : undefined, + preferredColumns: facetSpec.preferredColumns, + }; + + // --- layout ------------------------------------------------------------- + const density = theme.layout?.density ?? 'normal'; + const padding = density === 'compact' ? 8 : density === 'airy' ? 20 : 12; + + return { + themeId: theme.id, + surface: { canvas, plot, panel }, + text, + font: bodyFamily, + title: { + anchor: theme.layout?.titleBlock?.anchor ?? 'start', + headline, + deck, + }, + axes, + frame, + baseline, + series, + legend: { + show: legendShow, + placement, + ...(fallbacks.length ? { fallbacks } : {}), + orient: legendOrient, + direction: theme.legend?.direction + ?? (legendOrient === 'top' || legendOrient === 'bottom' ? 'horizontal' : 'vertical'), + title: legendTitle, + label: keyLabel, + gradientLength: legendSpec.gradientLength, + maxSwatches: legendSpec.maxSwatches, + }, + dataLabels: { + show: dlShow, + placement: dlPlacement, + inkMode: dlInkMode, + text: valueLabel, + format: numberFormat, + ...(valueUnit ? { unit: valueUnit } : {}), + insideMinValue, + outsideMaxValue, + }, + // A house that dots the end of a line is saying where the story stops. + // Only the policy is decided here: whether the chart *has* a line to + // dot is a question about the backend spec, and stage 3 answers it. + pointEmphasis: (theme.annotation?.pointEmphasis ?? 'never') !== 'never' + ? { + where: theme.annotation!.pointEmphasis as 'endpoints' | 'latest' | 'extremes', + labels: theme.annotation?.pointLabels ?? 'never', + size: (marks.strokeWidth || 2) * 14, + } + : undefined, + marks, + facets, + layout: { padding, density }, + statistics: theme.annotation?.statistics?.show?.length + ? { + show: theme.annotation.statistics.show, + placement: theme.annotation.statistics.placement ?? 'panel', + ...axisLabelText, + } + : undefined, + furniture: theme.furniture ?? [], + bound: { + measureChannels: bindings.measureChannels, + categoricalChannel: bindings.categoricalChannel, + seriesChannel: bindings.seriesChannel, + seriesField, + categoryField: catField, + seriesCount: signals.seriesCount, + categoryCount: signals.categoryCount, + isFaceted: signals.isFaceted, + isPartToWhole: signals.isPartToWhole, + isSigned: signals.isSigned, + markChannel: signals.markChannel, + }, + report, + }; +} + +// --------------------------------------------------------------------------- +// Series ink +// --------------------------------------------------------------------------- + +/** WCAG contrast between two colours, `1` for identical. */ +function contrastRatio(a: string, b: string): number { + const ca = parseColor(a); + const cb = parseColor(b); + if (!ca || !cb) return 21; + const la = luminance(ca); + const lb = luminance(cb); + return (Math.max(la, lb) + 0.05) / (Math.min(la, lb) + 0.05); +} + +/** The least contrast at which a filled cell still reads as a cell. */ +const ENDPOINT_CONTRAST = 1.2; + +/** + * Keep a ramp's ends off the page. + * + * A ramp that starts a shade away from the surface makes its smallest values + * disappear, and “least” then looks like “no data”. Where the house asks for + * endpoints that stand against the surface, an end too close to it is pulled + * away until it can be seen — the hue is the house's, only its distance from + * the page is not. + */ +function offSurface( + ramp: Ramp | undefined, + surface: string, + say: (path: string, message: string) => void, +): Ramp | undefined { + if (!ramp?.stops?.length || ramp.endpointsAgainstSurface !== true) return ramp; + const away = isDarkSurface(surface) ? '#ffffff' : '#000000'; + const stops = ramp.stops.slice(); + let moved = false; + for (const i of [0, stops.length - 1]) { + if (contrastRatio(stops[i], surface) >= ENDPOINT_CONTRAST) continue; + for (let t = 0.05; t <= 0.6; t += 0.05) { + const candidate = mixHex(stops[i], away, t, stops[i]); + if (contrastRatio(candidate, surface) >= ENDPOINT_CONTRAST) { + stops[i] = candidate; + moved = true; + break; + } + } + } + if (!moved) return ramp; + say('ink.series.endpointsAgainstSurface', + 'a ramp end sat too close to the surface to be seen as a value — it was pulled away from the page'); + return { ...ramp, stops }; +} + +function groundSeriesInk( + theme: ThemeSpec, + ctx: GroundingContext, + bindings: Bindings, + signals: Signals, + say: (path: string, message: string) => void, +): ResolvedSeriesInk { + const s = theme.ink.series ?? {}; + const selection = s.selection ?? {}; + const categorical = s.categorical ?? []; + const single = s.single ?? categorical[0] ?? theme.ink.accent ?? '#4c78a8'; + const surfaceColour = theme.ink.surface?.plot ?? theme.ink.surface?.canvas ?? '#ffffff'; + + const seriesChannel = bindings.seriesChannel; + const seriesField = channelFact(ctx, seriesChannel)?.field; + const seriesType = channelFact(ctx, seriesChannel)?.type; + const count = signals.seriesCount; + + const base: ResolvedSeriesInk = { + mode: 'single', + single, + categorical, + overflow: s.overflow, + status: s.status, + }; + + if (!seriesField || (signals.seriesCountKnown && count <= 1)) { + // A single-series chart still needs an ink, and there is exactly one + // right answer: the house's single-series colour. + return base; + } + if (!signals.seriesCountKnown) { + say('ink.series', + `\`${seriesField}\` is created by a backend transform — the whole categorical set is offered rather than guessing a count`); + } + + const facetField = bindings.facetChannel ? ctx.channelSemantics[bindings.facetChannel]?.field : undefined; + if (selection.redundantWithFacet === 'single' && facetField && facetField === seriesField) { + say('ink.series.selection.redundantWithFacet', + 'series colour collapsed to single — the facet already names the series'); + return base; + } + + // Continuous series: a ramp, not an indexed set. + if (seriesType === 'quantitative') { + const diverging = signals.isSigned && Boolean(s.diverging); + const ramp: Ramp | undefined = offSurface(diverging ? s.diverging : s.sequential, surfaceColour, say); + if (ramp?.stops?.length) { + const consumption = ramp.consumption ?? 'interpolate'; + const quantize = consumption === 'quantize' ? (ramp.quantizeCount ?? 5) : undefined; + return { + ...base, + mode: diverging ? 'diverging' : 'sequential', + ramp, + quantize, + range: quantize ? sampleRamp(ramp.stops, quantize) : ramp.stops.slice(), + }; + } + say('ink.series', 'no ramp declared for a continuous series — one is built from the house ink'); + // An indexed set is never the right answer for a continuous field: it + // says "different" where the data says "more". If the house has not + // declared a ramp, the honest fallback is a ramp of its own colour, + // from a tint of it to the colour itself. + const surface = theme.ink.surface?.canvas ?? '#ffffff'; + const stops = [mixHex(single, surface, 0.85), single]; + return { + ...base, + mode: 'sequential', + ramp: { stops }, + range: stops, + }; + } + + if (signals.isPartToWhole && selection.partToWhole === 'sequentialRamp' && s.sequential?.stops?.length) { + // One ramp, consumed as an indexed set: the largest share takes the + // darkest end, so the ramp is sampled in reverse. + const ramp = offSurface(s.sequential, surfaceColour, say)!; + const range = sampleRamp(ramp.stops, Math.max(2, count)).reverse(); + return { ...base, mode: 'sequential', ramp, range }; + } + + if (signals.isSigned && s.status && selection.signed === 'status' && selection.statusUse !== 'never') { + if (selection.statusUse === 'thresholdOnly') { + say('ink.series.selection.statusUse', + 'status ink withheld — `thresholdOnly` and no threshold was declared'); + } else { + return { ...base, mode: 'status' }; + } + } + + if (signals.isSigned && selection.signed === 'diverging' && s.diverging?.stops?.length) { + const ramp = offSurface(s.diverging, surfaceColour, say)!; + return { + ...base, + mode: 'diverging', + ramp, + range: sampleRamp(ramp.stops, Math.max(2, count)), + }; + } + + // An *ordered* series is not an indexed set. Categories that run from + // "a great deal" to "none at all" have a direction, and an unordered + // palette throws it away. One ramp, sampled to the number of steps. + if (seriesType === 'ordinal') { + const ramp: Ramp | undefined = offSurface( + s.sequential?.stops?.length ? s.sequential : s.diverging, surfaceColour, say); + if (ramp?.stops?.length && signals.seriesCountKnown) { + say('ink.series', 'the series is ordered — the house ramp is sampled across it rather than an unordered set'); + return { + ...base, + mode: 'sequential', + ramp, + range: sampleRamp(ramp.stops, Math.max(2, count)), + }; + } + } + + if (signals.seriesCountKnown && count > categorical.length && categorical.length > 0) { + if (s.overflow) { + say('ink.series.categorical', + `${count} series but the house declares ${categorical.length} — the rest take the overflow ink`); + } else if (theme.chartDefaults?.[ctx.chartType]?.showSeriesInLabel === true) { + // The house prints the series name on the mark for this kind of + // chart, so the names are already on the page in words. Colour was + // never the key here; keeping a foreign palette only spreads seven + // hues across seven lines and seven labels that say the same thing + // the words do. One ink, and the reader reads the names. + say('ink.series.categorical', + `${count} series against ${categorical.length} house inks, but the house names them on the mark — colour stops naming and takes the single ink`); + return { ...base, mode: 'single' }; + } else { + // An indexed set has a capacity, and past it the colours stop + // being names: two different series come out the same ink and the + // key lies. A house that declares six and no overflow ink has not + // said what the twenty-fifth thing looks like, and cycling is not + // an answer — it is the same answer twice. + say('ink.series.categorical', + `${count} series and the house declares ${categorical.length} with no overflow ink — colour cannot name them all, so the scale already on the chart stands`); + return { ...base, mode: 'categorical', exhausted: true }; + } + } + return { ...base, mode: 'categorical' }; +} + +// --------------------------------------------------------------------------- +// Redundant encoding +// --------------------------------------------------------------------------- + +/** + * Shape and dash exist to carry the series identity when colour cannot: in + * mono print, for a colour-blind reader, or simply when there are more series + * than the house has inks. They are only meaningful for an indexed set — a + * ramp is read as a quantity, and doubling it with shapes says nothing. + */ +function groundRedundancy( + marksSpec: NonNullable, + series: ResolvedSeriesInk, + signals: Signals, + say: (path: string, message: string) => void, +): { shape: boolean; dash: boolean } { + const off = { shape: false, dash: false }; + const policy = marksSpec.redundantEncoding ?? 'never'; + const channels = marksSpec.redundantChannels ?? []; + if (policy === 'never' || channels.length === 0) return off; + if (series.mode !== 'categorical') return off; + const effectiveCount = signals.seriesCountKnown ? signals.seriesCount : series.categorical.length; + if (effectiveCount <= 1) return off; + + if (policy === 'whenNeeded') { + const strained = effectiveCount > series.categorical.length; + if (!strained) { + say('marks.redundantEncoding', + '`whenNeeded` withheld — the house has a distinct ink for every series'); + return off; + } + } + const unsupported = channels.filter((c) => c === 'texture' || c === 'lightness'); + if (unsupported.length) { + say('marks.redundantChannels', `${unsupported.join(', ')} not realizable — ignored`); + } + return { shape: channels.includes('shape'), dash: channels.includes('dash') }; +} + +// --------------------------------------------------------------------------- +// Number format +// --------------------------------------------------------------------------- + +function groundNumberFormat( + theme: ThemeSpec, + ctx: GroundingContext, + measureChannel: 'x' | 'y' | undefined, +): string | undefined { + const nf = theme.annotation?.numberFormat; + if (!nf) return undefined; + const sem = measureChannel ? ctx.channelSemantics[measureChannel] : undefined; + const isPercent = typeof sem?.format?.suffix === 'string' && sem.format.suffix.includes('%'); + + const sign = nf.signed ? '+' : ''; + if (nf.thousands === 'suffix') return `${sign}~s`; + const group = nf.thousands === 'separator' ? ',' : ''; + const precision = nf.precision === 'integer' ? '.0' + : nf.precision === 'one' ? '.1' + : nf.precision === 'two' ? '.2' + : undefined; + if (precision === undefined) return group ? `${sign}${group}` : undefined; + return `${sign}${group}${precision}${isPercent ? 'f' : 'f'}`; +} + +function clamp(v: number, lo: number, hi: number): number { + return Math.max(lo, Math.min(hi, v)); +} diff --git a/packages/flint-js/src/core/theme/index.ts b/packages/flint-js/src/core/theme/index.ts new file mode 100644 index 00000000..ed38928c --- /dev/null +++ b/packages/flint-js/src/core/theme/index.ts @@ -0,0 +1,8 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +export * from './types.js'; +export * from './presence.js'; +export { groundTheme } from './ground.js'; +export type { GroundingContext } from './ground.js'; +export { THEME_PRESETS, listThemePresets, resolveThemeSpec } from './presets.js'; diff --git a/packages/flint-js/src/core/theme/presence.ts b/packages/flint-js/src/core/theme/presence.ts new file mode 100644 index 00000000..fca54f62 --- /dev/null +++ b/packages/flint-js/src/core/theme/presence.ts @@ -0,0 +1,203 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** + * The presence ordinal, resolved. + * + * `omit < hairline < quiet < full < emphasised` is a statement about *contrast + * against the surface the element sits on*, not about opacity or colour. That + * is the whole reason one ThemeSpec works on white and on Power BI's #1b1a19 + * with no branch: the ordinal is resolved late, against the surface that + * grounding actually chose. + */ + +import type { Presence } from './types.js'; + +export interface RGB { r: number; g: number; b: number; a: number } + +const HEX3 = /^#([0-9a-f])([0-9a-f])([0-9a-f])$/i; +const HEX6 = /^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i; +const HEX8 = /^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i; + +export function parseColor(input: string | undefined | null): RGB | null { + if (!input) return null; + const s = String(input).trim(); + let m = HEX8.exec(s); + if (m) { + return { + r: parseInt(m[1], 16), + g: parseInt(m[2], 16), + b: parseInt(m[3], 16), + a: parseInt(m[4], 16) / 255, + }; + } + m = HEX6.exec(s); + if (m) { + return { r: parseInt(m[1], 16), g: parseInt(m[2], 16), b: parseInt(m[3], 16), a: 1 }; + } + m = HEX3.exec(s); + if (m) { + return { + r: parseInt(m[1] + m[1], 16), + g: parseInt(m[2] + m[2], 16), + b: parseInt(m[3] + m[3], 16), + a: 1, + }; + } + return null; +} + +export function toHex(c: RGB): string { + const h = (n: number) => Math.max(0, Math.min(255, Math.round(n))).toString(16).padStart(2, '0'); + return `#${h(c.r)}${h(c.g)}${h(c.b)}`; +} + +/** Composite a possibly-transparent ink over an opaque surface. */ +export function flatten(ink: RGB, surface: RGB): RGB { + if (ink.a >= 1) return { ...ink, a: 1 }; + return { + r: ink.r * ink.a + surface.r * (1 - ink.a), + g: ink.g * ink.a + surface.g * (1 - ink.a), + b: ink.b * ink.a + surface.b * (1 - ink.a), + a: 1, + }; +} + +/** Linear blend from `a` (t=0) to `b` (t=1). */ +export function mix(a: RGB, b: RGB, t: number): RGB { + const k = Math.max(0, Math.min(1, t)); + return { + r: a.r + (b.r - a.r) * k, + g: a.g + (b.g - a.g) * k, + b: a.b + (b.b - a.b) * k, + a: 1, + }; +} + +export function mixHex(a: string, b: string, t: number, fallback = '#000000'): string { + const ca = parseColor(a); + const cb = parseColor(b); + if (!ca || !cb) return fallback; + return toHex(mix(ca, cb, t)); +} + +/** WCAG relative luminance. */ +export function luminance(c: RGB): number { + const f = (v: number) => { + const x = v / 255; + return x <= 0.03928 ? x / 12.92 : Math.pow((x + 0.055) / 1.055, 2.4); + }; + return 0.2126 * f(c.r) + 0.7152 * f(c.g) + 0.0722 * f(c.b); +} + +export function isDarkSurface(surface: string): boolean { + const c = parseColor(surface); + return c ? luminance(c) < 0.45 : false; +} + +/** + * How far up the ordinal an element sits, expressed as the fraction of the + * distance from the surface to full-strength role ink. + * + * These are the numbers in `fields.md` §presence, restated as a blend factor: + * `full` means "the ink the house declared", and everything below it is that + * same ink pulled back toward the surface. Nothing here invents a hue — the + * hue always comes from `ink.structure.*`. + */ +const PRESENCE_STRENGTH: Record = { + omit: 0, + hairline: 0.42, + quiet: 0.72, + full: 1, + emphasised: 1, +}; + +/** + * Contrast targets used only when the house declared no ink for the role. + * Expressed against the foreground, so a dark surface flips automatically. + */ +const PRESENCE_FALLBACK_CONTRAST: Record = { + omit: 0, + hairline: 0.06, + quiet: 0.12, + full: 0.4, + emphasised: 1, +}; + +export const PRESENCE_ORDER: Presence[] = ['omit', 'hairline', 'quiet', 'full', 'emphasised']; + +export function presenceRank(p: Presence | undefined, dflt: Presence = 'full'): number { + return PRESENCE_ORDER.indexOf(p ?? dflt); +} + +export interface ResolveInkArgs { + presence: Presence | undefined; + /** The surface this element sits on, already resolved. */ + surface: string; + /** `ink.structure.*` for this role, if the house declared one. */ + roleInk?: string; + /** Full-strength foreground, used for `emphasised` and as ink fallback. */ + foreground: string; + fallback?: Presence; +} + +/** + * Resolve a presence to a concrete colour, or `null` for `omit`. + * + * `emphasised` deliberately ignores the role ink: it means "as strong as text", + * and text ink is the only thing that knows what that is on this surface. + */ +export function resolvePresenceInk(args: ResolveInkArgs): string | null { + const p = args.presence ?? args.fallback ?? 'full'; + if (p === 'omit') return null; + + const surface = parseColor(args.surface) ?? { r: 255, g: 255, b: 255, a: 1 }; + const fg = parseColor(args.foreground) ?? { r: 0, g: 0, b: 0, a: 1 }; + + if (p === 'emphasised') return toHex(fg); + + const declared = parseColor(args.roleInk ?? undefined); + if (declared) { + // A fully transparent declared ink is the house saying "not here". + if (declared.a === 0) return null; + const target = flatten(declared, surface); + return toHex(mix(surface, target, PRESENCE_STRENGTH[p])); + } + return toHex(mix(surface, fg, PRESENCE_FALLBACK_CONTRAST[p])); +} + +/** Stroke width implied by the ordinal, in px. */ +export function presenceWidth(p: Presence | undefined, base = 1): number { + switch (p ?? 'full') { + case 'omit': return 0; + case 'hairline': return Math.min(base, 0.5); + case 'quiet': return base; + case 'full': return base; + case 'emphasised': return base * 1.5; + default: return base; + } +} + +/** Pick whichever of two inks reads better on `background`. */ +export function contrastingInk(background: string, light: string, dark: string): string { + const bg = parseColor(background); + if (!bg) return dark; + return luminance(bg) < 0.5 ? light : dark; +} + +/** Sample `n` evenly spaced colours from a ramp's control points. */ +export function sampleRamp(stops: string[], n: number): string[] { + if (stops.length === 0) return []; + if (n <= 1) return [stops[stops.length - 1]]; + const parsed = stops.map((s) => parseColor(s)).filter(Boolean) as RGB[]; + if (parsed.length === 0) return []; + if (parsed.length === 1) return new Array(n).fill(toHex(parsed[0])); + const out: string[] = []; + for (let i = 0; i < n; i++) { + const t = (i / (n - 1)) * (parsed.length - 1); + const lo = Math.floor(t); + const hi = Math.min(parsed.length - 1, lo + 1); + out.push(toHex(mix(parsed[lo], parsed[hi], t - lo))); + } + return out; +} diff --git a/packages/flint-js/src/core/theme/presets.ts b/packages/flint-js/src/core/theme/presets.ts new file mode 100644 index 00000000..63b8a528 --- /dev/null +++ b/packages/flint-js/src/core/theme/presets.ts @@ -0,0 +1,52 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** + * The houses Flint ships. + * + * Each is measured from a hand-authored redesign of real charts, so naming one + * is a claim that can be checked against the original rather than a mood. + * A caller who wants their own house passes a `ThemeSpec` object instead. + */ + +import type { ThemePreset, ThemeSpec } from './types'; +import { nyt } from './presets/nyt'; +import { economist } from './presets/economist'; +import { nature } from './presets/nature'; +import { mckinsey } from './presets/mckinsey'; +import { datawrapper } from './presets/datawrapper'; +import { powerbi } from './presets/powerbi'; + +export const THEME_PRESETS: Record = { + nyt, + economist, + nature, + mckinsey, + datawrapper, + powerbi, +}; + +/** The catalogue, without the specs — enough to choose by. */ +export function listThemePresets(): Array> { + return Object.values(THEME_PRESETS).map(({ id, label, description }) => ({ id, label, description })); +} + +/** + * Take what the caller put in `theme_spec` and hand back a ThemeSpec. + * + * A string names a house Flint ships; an object is the caller's own. An + * unknown name is an error rather than a silent fallback to no theme: a chart + * that quietly ignores the house it was asked for looks like a bug in the + * house. + */ +export function resolveThemeSpec(theme: ThemeSpec | string | undefined): ThemeSpec | undefined { + if (theme === undefined) return undefined; + if (typeof theme !== 'string') return theme; + const preset = THEME_PRESETS[theme]; + if (!preset) { + throw new Error( + `Unknown theme \`${theme}\`. Flint ships: ${Object.keys(THEME_PRESETS).join(', ')}.`, + ); + } + return preset.spec; +} diff --git a/packages/flint-js/src/core/theme/presets/datawrapper.ts b/packages/flint-js/src/core/theme/presets/datawrapper.ts new file mode 100644 index 00000000..68ab6f5b --- /dev/null +++ b/packages/flint-js/src/core/theme/presets/datawrapper.ts @@ -0,0 +1,164 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import type { ThemePreset } from '../types'; + +/** + * Datawrapper. + * + * Measured from hand-authored redesigns, not invented — see the Theme Lab. + */ +export const datawrapper: ThemePreset = { + id: 'datawrapper', + label: "Datawrapper", + description: "Embedded web chart: narrow column, plain headline and deck, a rule under the footer.", + guidance: [ + "- `title` and `subtitle` do all the naming; annotate the measure with `unit`.", + "- Sized for a narrow column, so it reads tall rather than wide.", + "- Colour can tell 5 categories apart.", + ].join('\n'), + spec: { + "id": "datawrapper", + "label": "Datawrapper", + "ink": { + "surface": { + "source": "host" + }, + "text": { + "primary": "#333333", + "secondary": "#666666", + "muted": "#999999" + }, + "structure": { + "grid": "#dcdcdc", + "rule": "#dcdcdc", + "connector": "#c8c8c8", + "axis": "#333333" + }, + "series": { + "single": "#18a1cd", + "categorical": [ + "#18a1cd", + "#e2a233", + "#c04a4a", + "#2d8659", + "#7e5aa2" + ], + "sequential": { + "stops": [ + "#dceef6", + "#a9d3e6", + "#6aabcc", + "#2f7fa8", + "#0b5c82" + ], + "space": "lab", + "consumption": "quantize", + "quantizeCount": 5 + }, + "diverging": { + "stops": [ + "#2f7fa8", + "#a9d3e6", + "#f0ece4", + "#e8ac70", + "#c04a4a" + ], + "neutral": "#f0ece4", + "space": "lab", + "endpointsAgainstSurface": true, + "consumption": "quantize", + "quantizeCount": 5 + }, + "selection": {} + }, + "accent": "#18a1cd" + }, + "type": { + "minSize": 11, + "headline": { + "family": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "size": "text.300", + "weight": "bold" + }, + "axisLabel": { + "size": "text.200" + }, + "keyLabel": { + "size": "text.200" + } + }, + "structure": { + "axis": { + "categorical": { + "line": "full", + "ticks": "omit", + "tickLabels": "sparse" + }, + "measure": { + "line": "omit", + "ticks": "omit" + } + }, + "grid": { + "measure": "quiet", + "category": "omit", + "style": "dashed" + }, + "frame": "omit", + "baseline": "quiet" + }, + "marks": { + "bandFraction": 0.66, + "separator": { + "presence": "hairline", + "source": "surface", + "width": 1.5 + }, + "connector": { + "presence": "full", + "weight": 1 + } + }, + "labels": { + "truncation": "never" + }, + "legend": { + "show": "always", + "placement": [ + "top" + ], + "direction": "horizontal", + "title": "omit" + }, + "dataLabels": { + "show": "whenTheyFit", + "placement": "outsideMark" + }, + "annotation": { + "axisTitles": "omit", + "unit": "lastTick", + "numberFormat": { + "precision": "auto" + } + }, + "furniture": [ + { + "kind": "footerRule", + "anchor": "bottomLeft", + "color": "#dcdcdc", + "height": 1 + } + ], + "interaction": { + "tooltipFormat": "matchKey" + }, + "layout": { + "density": "normal", + "targetWidth": 300, + "titleBlock": { + "anchor": "start" + } + } + }, +}; diff --git a/packages/flint-js/src/core/theme/presets/economist.ts b/packages/flint-js/src/core/theme/presets/economist.ts new file mode 100644 index 00000000..eb9e243e --- /dev/null +++ b/packages/flint-js/src/core/theme/presets/economist.ts @@ -0,0 +1,200 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import type { ThemePreset } from '../types'; + +/** + * The Economist. + * + * Measured from hand-authored redesigns, not invented — see the Theme Lab. + */ +export const economist: ThemePreset = { + id: 'economist', + label: "The Economist", + description: "Print weekly: compact, flat headline over a deck that names the measure, units repeated down the ruler.", + guidance: [ + "- `subtitle` names the measure, the place and the period — \"% of GDP, 2023\".", + "- Annotate each measure with `unit` in `semantic_types`.", + "- The key holds 3 colours.", + ].join('\n'), + spec: { + "id": "economist", + "label": "The Economist", + "ink": { + "surface": { + "source": "host" + }, + "text": { + "primary": "#121317", + "secondary": "#54585a", + "muted": "#8b9196" + }, + "structure": { + "grid": "#c9d3da", + "axis": "#121317", + "rule": "#c9d3da" + }, + "series": { + "single": "#006ba2", + "categorical": [ + "#3f5661", + "#a1655a", + "#006ba2", + "#7ba7b8", + "#3ebcd2", + "#c8b88a" + ], + "diverging": { + "stops": [ + "#006ba2", + "#7ba7b8", + "#e9e5dc", + "#c8967a", + "#a1655a" + ], + "neutral": "#e9e5dc", + "space": "lab", + "endpointsAgainstSurface": true, + "consumption": "interpolate" + }, + "status": { + "positive": "#006ba2", + "negative": "#e3120b", + "neutral": "#b8c4cc" + }, + "selection": { + "signed": "status", + "statusUse": "anySigned" + } + }, + "accent": "#e3120b" + }, + "type": { + "minSize": 8, + "headline": { + "family": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "size": "text.300", + "weight": "bold" + }, + "deck": { + "size": "text.200", + "color": "#54585a" + }, + "axisLabel": { + "size": "text.100" + } + }, + "structure": { + "axis": { + "categorical": { + "line": "full", + "ticks": "omit" + }, + "measure": { + "line": "omit", + "ticks": "omit", + "placement": "default" + } + }, + "grid": { + "measure": "quiet", + "category": "omit", + "style": "solid", + "zero": "full" + }, + "frame": "omit", + "baseline": "full" + }, + "marks": { + "bandFraction": 0.68, + "strokeWeight": 1.6, + "slice": { + "gap": 1.5 + }, + "interval": { + "fillOpacity": 0.22, + "edge": "quiet", + "inkSource": "sameAsCentral" + }, + "sizeRange": [ + 10, + 450 + ] + }, + "labels": { + "truncation": "never", + "angle": "auto" + }, + "legend": { + "show": "always", + "placement": [ + "seriesEnd", + "top" + ], + "direction": "horizontal", + "title": "omit", + "maxSwatches": 3 + }, + "dataLabels": { + "show": "whenTheyFit", + "placement": "atMark" + }, + "annotation": { + "axisTitles": "omit", + "unit": "everyTick" + }, + "furniture": [ + { + "kind": "mastheadTab", + "anchor": "topLeft", + "color": "#e3120b", + "width": 26, + "height": 3 + } + ], + "layout": { + "density": "compact", + "titleBlock": { + "anchor": "start" + } + }, + "variants": [ + { + "when": { + "markChannel": "length" + }, + "then": { + "structure": { + "axis": { + "measure": { + "placement": "opposite" + } + } + } + }, + "because": "Measured: big-mac, causes-death and state-jobless all carry the measure axis opposite (3 of 3 bar charts). On a banded chart the far edge is where a reader enters." + }, + { + "when": { + "isPartToWhole": true + }, + "then": { + "structure": { + "axis": { + "measure": { + "placement": "opposite" + } + } + } + }, + "because": "Measured: electricity-mix-area puts y on the right, seattle-range does not. Both are area marks, so markChannel cannot separate them; isPartToWhole can. On a pie the policy is inert." + } + ], + "chartDefaults": { + "Slope Chart": { + "showText": true, + "showSeriesInLabel": true + } + } + }, +}; diff --git a/packages/flint-js/src/core/theme/presets/mckinsey.ts b/packages/flint-js/src/core/theme/presets/mckinsey.ts new file mode 100644 index 00000000..31d7274a --- /dev/null +++ b/packages/flint-js/src/core/theme/presets/mckinsey.ts @@ -0,0 +1,156 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import type { ThemePreset } from '../types'; + +/** + * McKinsey. + * + * Measured from hand-authored redesigns, not invented — see the Theme Lab. + */ +export const mckinsey: ThemePreset = { + id: 'mckinsey', + label: "McKinsey", + description: "Consulting deck: wide bands, every value printed in a column, a headline that states the takeaway.", + guidance: [ + "- `title` states the takeaway; `subtitle` names the measure and unit.", + "- Colour can tell 5 categories apart.", + ].join('\n'), + spec: { + "id": "mckinsey", + "label": "McKinsey", + "ink": { + "surface": { + "source": "host" + }, + "text": { + "primary": "#051c2c", + "secondary": "#5a6872", + "muted": "#8a969d" + }, + "structure": { + "axis": "#051c2c", + "rule": "#d3dce1" + }, + "series": { + "single": "#051c2c", + "categorical": [ + "#051c2c", + "#2251ff", + "#00a9f4", + "#00cfb4", + "#8c9ba5" + ], + "sequential": { + "stops": [ + "#eef3f8", + "#cfdcea", + "#9db8d2", + "#5b82ab", + "#051c2c" + ], + "space": "lab", + "endpointsAgainstSurface": true, + // Stated light-to-dark. One ramp, two consumptions: a + // part-to-whole pie samples it in reverse, a heat map + // interpolates it. + "consumption": "interpolate" + }, + "selection": { + "partToWhole": "sequentialRamp", + "signed": "sequential" + } + }, + "accent": "#2251ff" + }, + "type": { + "minSize": 9, + "headline": { + "family": "'Helvetica Neue', Helvetica, Arial, sans-serif", + "size": "text.300", + "weight": "bold" + }, + "axisLabel": { + "size": "text.200" + }, + "valueLabel": { + "size": "text.200", + "weight": "semibold", + "color": "#051c2c" + } + }, + "structure": { + "axis": { + "categorical": { + "line": "omit", + "ticks": "omit" + }, + "measure": { + "line": "omit", + "ticks": "omit", + "suppressWhenValuesPrinted": true + } + }, + "grid": { + "measure": "omit", + "category": "omit" + }, + "frame": "omit", + "baseline": "full" + }, + "marks": { + "bandFraction": 0.6, + "strokeWeight": 2, + "connector": { + "presence": "full", + "weight": 0.8, + "spanWeight": 3 + }, + "point": { + "size": 64 + }, + "separator": { + "presence": "hairline", + "source": "surface", + "width": 0.6 + }, + "slice": { + "gap": 1 + } + }, + "labels": { + "truncation": "never", + "angle": "horizontal" + }, + "legend": { + "show": "always", + "placement": [ + "seriesEnd", + "inline", + "top" + ], + "direction": "horizontal", + "title": "omit", + "suppressWhenValuesPrinted": true + }, + "dataLabels": { + "show": "always", + "placement": "column", + "inkMode": "contrastWithMark" + }, + "annotation": { + "axisTitles": "omit", + "numberFormat": { + "precision": "integer", + "thousands": "separator" + } + }, + "layout": { + "density": "airy", + "titleBlock": { + "anchor": "start" + }, + "bandStep": 80 + } + }, +}; diff --git a/packages/flint-js/src/core/theme/presets/nature.ts b/packages/flint-js/src/core/theme/presets/nature.ts new file mode 100644 index 00000000..8bb60f41 --- /dev/null +++ b/packages/flint-js/src/core/theme/presets/nature.ts @@ -0,0 +1,197 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import type { ThemePreset } from '../types'; + +/** + * Nature. + * + * Measured from hand-authored redesigns, not invented — see the Theme Lab. + */ +export const nature: ThemePreset = { + id: 'nature', + label: "Nature", + description: "Journal figure: small panel, axis titles kept with units, statistics printed beside the fit.", + guidance: [ + "- Annotate every measure with `unit` in `semantic_types`; `subtitle` names the sample, not the unit.", + "- Colour can tell 6 categories apart; past that they share a grey.", + ].join('\n'), + spec: { + "id": "nature", + "label": "Nature", + "ink": { + "surface": { + "source": "host" + }, + "text": { + "primary": "#000000", + "secondary": "#000000" + }, + "structure": { + "axis": "#000000", + "grid": "#00000000", + "frame": "#000000" + }, + "series": { + "single": "#0072b2", + "categorical": [ + "#0072b2", + "#e69f00", + "#009e73", + "#cc79a7", + "#56b4e9", + "#d55e00" + ], + "diverging": { + "stops": [ + "#0072b2", + "#83b9db", + "#ffffff", + "#eba06a", + "#d55e00" + ], + "neutral": "#ffffff", + "space": "lab", + "endpointsAgainstSurface": false, + "consumption": "interpolate" + }, + "overflow": "#999999", + "selection": { + "partToWhole": "categorical" + } + }, + "accent": "#000000" + }, + "type": { + "minSize": 8.5, + "headline": { + "family": "Arial, Helvetica, sans-serif", + "size": "text.200", + "weight": "bold" + }, + "deck": { + "size": "text.100", + "style": "italic", + "case": "asIs" + }, + "axisLabel": { + "family": "Arial, Helvetica, sans-serif", + "size": "text.100" + }, + "axisTitle": { + "family": "Arial, Helvetica, sans-serif", + "size": "text.100" + } + }, + "structure": { + "axis": { + "categorical": { + "line": "full", + "ticks": "full", + "tickLength": "long", + "tickDirection": "outward" + }, + "measure": { + "line": "full", + "ticks": "full", + "tickLength": "long", + "tickDirection": "outward" + } + }, + "grid": { + "measure": "omit", + "category": "omit" + }, + "frame": "omit", + "baseline": "quiet" + }, + "marks": { + "bandFraction": 0.55, + "strokeWeight": 1.2, + "separator": { + "presence": "hairline", + "source": "surface", + "width": 0.5 + }, + "slice": { + "gap": 1.5 + }, + "point": { + "presence": "full", + "size": 45, + "fill": "solid", + "halo": { + "presence": "hairline", + "width": 0.6 + } + }, + "interval": { + "fillOpacity": 0.25, + "edge": "omit", + "inkSource": "sameAsCentral" + }, + "summary": { + "fill": "omit", + "outline": "full", + "centralRule": "emphasised", + "widthFraction": 0.4 + }, + "observations": { + "expose": "always", + "maxRows": 400 + }, + "zOrder": "summaryUnderData", + "redundantEncoding": "always", + "redundantChannels": [ + "shape" + ] + }, + "labels": { + "truncation": "never" + }, + "legend": { + "show": "always", + "placement": [ + "right", + "inside" + ], + "title": "whenAmbiguous", + "suppressWhenAxisNames": true + }, + "dataLabels": { + "show": "whenTheyFit", + "placement": "outsideMark" + }, + "annotation": { + "axisTitles": "always", + "axisTitlePlacement": "rotated", + "unitsInAxisTitle": true, + "statistics": { + "show": [ + "n", + "r2", + "slope" + ], + "placement": "panel" + } + }, + "layout": { + "density": "compact", + "targetWidth": 252, + "titleBlock": { + "anchor": "start" + }, + "bandStep": 46 + }, + "chartDefaults": { + "Boxplot": { + "showPoints": true + }, + "Violin Plot": { + "showPoints": true, + "showMedian": true, + "showContour": true + } + } + }, +}; diff --git a/packages/flint-js/src/core/theme/presets/nyt.ts b/packages/flint-js/src/core/theme/presets/nyt.ts new file mode 100644 index 00000000..579ab69a --- /dev/null +++ b/packages/flint-js/src/core/theme/presets/nyt.ts @@ -0,0 +1,178 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import type { ThemePreset } from '../types'; + +/** + * New York Times. + * + * Measured from hand-authored redesigns, not invented — see the Theme Lab. + */ +export const nyt: ThemePreset = { + id: 'nyt', + label: "New York Times", + description: "Newsroom graphics: a headline that states the finding, values printed on the marks, series named at their own ends.", + guidance: [ + "- `title` is the finding, in a sentence; `subtitle` names the measure, the population and the unit.", + "- Colour can tell 5 categories apart; past that they share a grey.", + ].join('\n'), + spec: { + "id": "nyt", + "label": "New York Times", + "ink": { + "surface": { + "source": "host" + }, + "text": { + "primary": "#121212", + "secondary": "#6b6b6b", + "muted": "#8a8a8a", + "inverse": "#ffffff" + }, + "structure": { + "grid": "#e4e4e4", + "axis": "#121212", + "rule": "#121212" + }, + "series": { + "single": "#2f6b9a", + "categorical": [ + "#2f6b9a", + "#c2352b", + "#4a8b6f", + "#7f6a9e", + "#d9a441" + ], + "diverging": { + "stops": [ + "#2f6b9a", + "#8fb4cc", + "#efece5", + "#dd9a86", + "#c2352b" + ], + "neutral": "#efece5", + "space": "lab", + "endpointsAgainstSurface": true, + "consumption": "interpolate" + }, + "overflow": "#9e9e9e", + "status": { + "positive": "#2f6b9a", + "negative": "#c2352b", + "neutral": "#9e9e9e" + }, + "selection": { + "signed": "status", + "statusUse": "anySigned" + } + }, + "accent": "#c2352b" + }, + "type": { + "minSize": 8, + "headline": { + "family": "Georgia, serif", + "size": "text.400", + "weight": "bold", + "color": "#121212" + }, + "deck": { + "family": "Georgia, serif", + "size": "text.200", + "color": "#6b6b6b" + }, + "axisLabel": { + "family": "Helvetica, Arial, sans-serif", + "size": "text.100" + }, + "valueLabel": { + "family": "Helvetica, Arial, sans-serif", + "size": "text.100", + "weight": "bold" + } + }, + "structure": { + "axis": { + "categorical": { + "line": "full", + "ticks": "omit", + "tickLabels": "sparse" + }, + "measure": { + "line": "omit", + "ticks": "omit", + "tickDensity": "sparse", + "suppressWhenValuesPrinted": true + } + }, + "grid": { + "measure": "quiet", + "category": "omit", + "style": "solid", + "zero": "full" + }, + "frame": "omit", + "baseline": "full" + }, + "marks": { + "bandFraction": 0.72, + "strokeWeight": 2.4, + "strokeCap": "round", + "strokeJoin": "round", + "slice": { + "gap": 1.5 + }, + "tile": { + "gap": 1 + }, + "zOrder": "summaryOverData", + "redundantEncoding": "whenNeeded", + "redundantChannels": [ + "dash" + ] + }, + "labels": { + "truncation": "never", + "flush": true + }, + "legend": { + "show": "always", + "placement": [ + "seriesEnd", + "top" + ], + "title": "omit", + "suppressWhenValuesPrinted": false + }, + "dataLabels": { + "show": "always", + "placement": "atMark", + "inkMode": "contrastWithMark" + }, + "annotation": { + "axisTitles": "omit", + "axisTitlePlacement": "flatAboveAxis", + "unit": "lastTick", + "pointEmphasis": "endpoints", + "numberFormat": { + "precision": "auto", + "thousands": "suffix" + } + }, + "layout": { + "density": "normal", + "titleBlock": { + "anchor": "start" + } + }, + "chartDefaults": { + "Line Chart": { + "showPoints": true + }, + "Bump Chart": { + "interpolate": "linear" + } + } + }, +}; diff --git a/packages/flint-js/src/core/theme/presets/powerbi.ts b/packages/flint-js/src/core/theme/presets/powerbi.ts new file mode 100644 index 00000000..7274f13b --- /dev/null +++ b/packages/flint-js/src/core/theme/presets/powerbi.ts @@ -0,0 +1,187 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import type { ThemePreset } from '../types'; + +/** + * Power BI. + * + * Measured from hand-authored redesigns, not invented — see the Theme Lab. + */ +export const powerbi: ThemePreset = { + id: 'powerbi', + label: "Power BI", + description: "Dashboard tile: compact, legend to the right, the latest point emphasised.", + guidance: [ + "- Leave `title` out where the tile sits under its own caption — the axis titles come back to name the measure.", + "- Colour can tell 6 categories apart.", + ].join('\n'), + spec: { + "id": "powerbi", + "label": "Power BI", + "ink": { + "surface": { + "source": "house", + "canvas": "#1b1a19", + "plot": "#1b1a19", + "panel": "#252423" + }, + "text": { + "primary": "#f3f2f1", + "secondary": "#c8c6c4", + "muted": "#a19f9d", + "inverse": "#1b1a19" + }, + "structure": { + "grid": "#3b3a39", + "axis": "#3b3a39", + "rule": "#3b3a39" + }, + "series": { + "single": "#118dff", + "categorical": [ + "#118dff", + "#12239e", + "#e66c37", + "#6b007b", + "#e044a7", + "#744ec2" + ], + "diverging": { + "stops": [ + "#118dff", + "#5aa9f0", + "#4a4948", + "#e08a4a", + "#d64550" + ], + "neutral": "#4a4948", + "space": "lab", + "endpointsAgainstSurface": true, + "consumption": "interpolate" + }, + "status": { + "positive": "#22b14c", + "negative": "#e66c37", + "neutral": "#a19f9d" + }, + "selection": { + "signed": "diverging", + "statusUse": "thresholdOnly", + "redundantWithFacet": "single" + } + }, + "accent": "#118dff" + }, + "type": { + "minSize": 8, + "headline": { + "family": "'Segoe UI', system-ui, sans-serif", + "size": "text.200", + "weight": "semibold", + "color": "#f3f2f1" + }, + "display": { + "family": "'Segoe UI', system-ui, sans-serif", + "size": "text.hero900", + "weight": "semibold" + }, + "axisLabel": { + "family": "'Segoe UI', system-ui, sans-serif", + "size": "text.100", + "color": "#c8c6c4" + }, + "keyLabel": { + "size": "text.100", + "color": "#c8c6c4" + } + }, + "structure": { + "axis": { + "categorical": { + "line": "omit", + "ticks": "omit", + "tickLabels": "sparse" + }, + "measure": { + "line": "omit", + "ticks": "omit", + "tickDensity": "sparse" + } + }, + "grid": { + "measure": "quiet", + "category": "omit", + "style": "dashed" + }, + "frame": "omit", + "baseline": "quiet" + }, + "marks": { + "strokeWeight": 2.2, + "strokeCap": "square", + "minSize": 1.5, + "separator": { + "presence": "hairline", + "source": "surface", + "width": 1 + }, + "slice": { + "gap": 1.5 + }, + "trailingFill": { + "presence": "quiet", + "opacity": 0.18 + }, + "reference": { + "presence": "full", + "style": "tick", + "label": true, + "weight": 2 + } + }, + "labels": { + "truncation": "never" + }, + "legend": { + "show": "always", + "placement": [ + "right", + "bottom" + ], + "title": "omit", + "gradientLength": 90, + "suppressWhenValuesPrinted": false + }, + "dataLabels": { + "show": "whenTheyFit", + "placement": "atMark", + "inkMode": "contrastWithMark" + }, + "annotation": { + "axisTitles": "omit", + "unit": "everyTick", + "pointEmphasis": "latest", + "numberFormat": { + "precision": "auto" + } + }, + "facets": { + "header": { + "presence": "full", + "style": "flushLabel", + "fieldTitle": "omit" + }, + "panelFrame": "omit", + "axisRepetition": "edgeOnly", + "preferredColumns": 4, + "sharedScale": "whenComparable" + }, + "layout": { + "density": "compact", + "titleBlock": { + "anchor": "start" + } + } + }, +}; diff --git a/packages/flint-js/src/core/theme/types.ts b/packages/flint-js/src/core/theme/types.ts new file mode 100644 index 00000000..24b79afb --- /dev/null +++ b/packages/flint-js/src/core/theme/types.ts @@ -0,0 +1,680 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** + * ThemeSpec — level 1 (authored) and level 2 (grounded) types. + * + * See `design-docs/03-themeSpec-abstract-design.md` and + * `design-docs/themespec/fields.md` for the argument behind every field. + * + * The invariant this file encodes: + * + * LEVEL 1 (`ThemeSpec`) never names a chart type, a positional channel, + * a mark type, a field, or a backend property. + * + * LEVEL 2 (`DesignDecisions`) has bound every role to a concrete part of + * *this* chart and resolved every policy against the space actually + * available — but is still backend-neutral. + * + * Level 3 (realization) lives in each backend, e.g. `vegalite/theme.ts`. + */ + +import type { AssembleOptions } from '../types.js'; + +// --------------------------------------------------------------------------- +// Level 1 — the authored ThemeSpec +// --------------------------------------------------------------------------- + +/** The presence ordinal. Grounding may step DOWN it, and must report. */ +export type Presence = 'omit' | 'hairline' | 'quiet' | 'full' | 'emphasised'; + +export type Frequency = 'never' | 'whenNeeded' | 'always'; + +/** A design-token size reference (`text.100`, `text.hero900`) or a raw px number. */ +export type SizeToken = string | number; + +export interface TypeRole { + family?: string; + size?: SizeToken; + weight?: 'regular' | 'medium' | 'semibold' | 'bold'; + /** Journals set decks and captions in italic; it is a role, not an accident. */ + style?: 'normal' | 'italic'; + case?: 'asIs' | 'upper' | 'lower' | 'title'; + color?: string; +} + +export interface AxisRole { + line?: Presence; + ticks?: Presence; + tickLength?: 'short' | 'medium' | 'long'; + tickDirection?: 'outward' | 'inward'; + /** `opposite` = the far side of the plot (top for x, right for y). */ + placement?: 'default' | 'opposite'; + tickLabels?: 'all' | 'observed' | 'endpoints' | 'sparse'; + tickDensity?: 'sparse' | 'normal' | 'dense'; + /** + * Drop the axis entirely when every mark already prints its own value. + * The scale is then carried by the numbers, not by a ruler beside them. + * Mirrors `legend.suppressWhenValuesPrinted`. + */ + suppressWhenValuesPrinted?: boolean; +} + +/** + * Control points of an interpolator — NOT an indexed set. + * Length is resolution, not capacity, so `overflow` does not apply. + */ +export interface Ramp { + stops: string[]; + /** Diverging only: the ink at the pivot. */ + neutral?: string; + space?: 'rgb' | 'lab' | 'hcl'; + /** Forbid the ramp endpoints colliding with the canvas. */ + endpointsAgainstSurface?: boolean; + consumption?: 'interpolate' | 'quantize' | 'sampleCategorical'; + quantizeCount?: number; +} + +export interface ThemeInk { + surface?: { + source?: 'host' | 'house'; + canvas?: string; + plot?: string; + panel?: string; + }; + text?: { + primary?: string; + secondary?: string; + muted?: string; + inverse?: string; + }; + /** The ink the presence ordinal scales against. */ + structure?: { + axis?: string; + grid?: string; + frame?: string; + rule?: string; + /** + * The stem of a lollipop, the bridge of a dumbbell. It borrows `rule` + * where the house says nothing, but the two are not the same job: a + * gridline is read *through*, so it sits at the bottom of the ordinal, + * while a connector is part of the mark and has to hold its shape at + * a hairline's width. A house whose gridlines are already pale has no + * room left below them, and states this ink instead. + */ + connector?: string; + }; + series?: { + single?: string; + categorical?: string[]; + overflow?: string; + sequential?: Ramp; + diverging?: Ramp; + status?: { positive?: string; negative?: string; neutral?: string }; + selection?: { + partToWhole?: 'categorical' | 'sequentialRamp'; + signed?: 'categorical' | 'status' | 'diverging' | 'sequential'; + redundantWithFacet?: 'single' | 'categorical'; + statusUse?: 'anySigned' | 'thresholdOnly' | 'never'; + }; + }; + accent?: string; +} + +export interface ThemeType { + minSize?: number; + headline?: TypeRole; + deck?: TypeRole; + axisLabel?: TypeRole; + axisTitle?: TypeRole; + valueLabel?: TypeRole; + keyLabel?: TypeRole; + annotation?: TypeRole; + footnote?: TypeRole; + /** The KPI big-number role — data, not chrome. */ + display?: TypeRole; +} + +export interface ThemeStructure { + axis?: { + categorical?: AxisRole; + measure?: AxisRole; + }; + /** + * Gridlines, bound by what the axis *does*, not by what it holds. + * + * `measure` is the grid the reader reads values off — the lines that run + * across the value axis. `category` is the grid across the axis the chart + * is indexed by, which on a scatter is the horizontal one even though it + * carries a number. Houses that rule only horizontally are asking for + * `measure` on and `category` off, whatever the two axes happen to hold. + */ + grid?: { + measure?: Presence; + category?: Presence; + style?: 'solid' | 'dashed' | 'dotted'; + /** + * A separate rule where the value axis crosses zero. `omit` leaves + * zero as an ordinary gridline; anything else draws it in its own + * weight. Only ever drawn when zero is inside the domain. + */ + zero?: Presence; + }; + frame?: Presence; + baseline?: Presence; +} + +export interface ThemeMarks { + bandFraction?: number; + strokeWeight?: number; + strokeCap?: 'butt' | 'round' | 'square'; + strokeJoin?: 'miter' | 'round' | 'bevel'; + interpolation?: 'linear' | 'monotone' | 'step'; + fillOpacity?: number; + sizeRange?: [number, number]; + minSize?: number; + zOrder?: 'summaryOverData' | 'summaryUnderData'; + separator?: { presence?: Presence; width?: number; source?: 'surface' | 'structure' }; + /** + * A wedge sits in no band, so how far apart neighbouring wedges stand is + * its own question. A house may rule its stacked bars with a half-pixel + * hairline and still want a clean cut between the pieces of a pie: two + * arcs of the same size at different orientations are hard enough to + * compare without also having to find where one ends. Where the house + * says nothing, wedges are held apart the way its bars are. + * + * `rule` paints the shared edge in the separator's ink, which reads as a + * gap of constant width. `pad` swings the wedges apart instead, so the + * gap opens at the rim and closes to nothing at the centre. + */ + slice?: { gap?: number; gapStyle?: 'rule' | 'pad' }; + /** + * How far apart the cells of a grid stand — a heatmap, a calendar, a + * matrix. A cell is not a bar: it has no band to give back, its two + * neighbours are on two axes, and its colour is the reading, so a gap + * between cells has to be cut out of the shape the way a wedge's is. + * + * Whether to cut at all is a real difference between houses and not a + * detail. Flush cells read as a continuous field — the eye follows the + * gradient across a row and sees a season. Cut cells read as a table of + * separate readings, which is what a house wants when it prints the number + * inside each one. Where the house says nothing, cells are held apart the + * way its bars are. + * + * The gap is painted, not spaced, and it takes the surface unless the + * house asks for structure: on a grid whose colour *is* the value, an edge + * in any other ink reads as data. + */ + tile?: { gap?: number; source?: 'surface' | 'structure' }; + point?: { + /** + * Whether a *line* carries a dot at each of its vertices. This is a + * question about lines, not about dots: a scatter's dots are drawn + * because the chart is a scatter, and no house presence turns them off. + */ + presence?: Presence; + /** + * How big a dot this house draws, as an area in px² — the way the + * renderer states a point's size and the way the size channel is read. + * One number, wherever a dot appears: at a line's vertex, on a scatter, + * at the ends of a dumbbell. A house that wanted its scatter dots and + * its vertex dots at different sizes would be saying that the same ink + * means two things, and the eye does not read them that way. + * + * Where the house says nothing the renderer's default stands. The + * layout remains free to shrink it when the plot runs short of room. + */ + size?: number; + fill?: 'solid' | 'hollow'; + halo?: { presence?: Presence; width?: number }; + }; + connector?: { + presence?: Presence; + /** + * The weight of a connector that runs from a mark to the baseline — a + * lollipop's stem. It restates nothing: the dot's position already + * carries the value and the stem only leads the eye down to the axis, + * so it is drawn as structure. + */ + weight?: number; + /** + * The weight of a connector that runs between two marks — a dumbbell's + * bridge. This one is not redundant: the *distance* it draws is the + * reading, and a hairline asks the eye to measure a gap it can barely + * see. It keeps structure's ink and takes a mark's weight. + * + * Where the house says nothing, a bridge is drawn at the weight the + * house gives its lines: it is a mark, so it takes a mark's weight. + */ + spanWeight?: number; + style?: 'solid' | 'dashed' | 'dotted'; + }; + trailingFill?: { presence?: Presence; opacity?: number }; + interval?: { fillOpacity?: number; edge?: Presence; inkSource?: 'sameAsCentral' | 'structure' }; + summary?: { + fill?: Presence; + outline?: Presence; + centralRule?: Presence; + widthFraction?: number; + }; + observations?: { expose?: 'never' | 'whenSparse' | 'always'; maxRows?: number }; + reference?: { presence?: Presence; style?: 'tick' | 'line' | 'dashed'; weight?: number; label?: boolean }; + redundantEncoding?: Frequency; + redundantChannels?: Array<'shape' | 'dash' | 'texture' | 'lightness'>; +} + +export interface ThemeLabels { + truncation?: 'never' | 'ellipsis' | 'wrap'; + flush?: boolean; + angle?: 'auto' | 'horizontal' | 'rotated'; +} + +export type LegendPlacement = + | 'seriesEnd' | 'inline' | 'top' | 'right' | 'bottom' | 'left' | 'inside'; + +export interface ThemeLegend { + show?: 'always' | 'never'; + placement?: LegendPlacement[]; + direction?: 'horizontal' | 'vertical'; + /** + * `whenAmbiguous` asks whether the key's labels say what they are: a list + * of names (`Chrome`, `Cairo`) does, and a ramp of numbers does not. + */ + title?: 'omit' | 'whenAmbiguous' | 'always'; + gradientLength?: number; + /** + * The most entries a key to *values* may spend. A legend that names ten + * bubble sizes is a table, not a key: three well-chosen sizes tell the + * reader the scale and leave the chart the room. + */ + maxSwatches?: number; + swatch?: 'auto'; + /** The legend restates the categorical axis — delete it. */ + suppressWhenAxisNames?: boolean; + /** The legend restates a number already printed in every mark — delete it. */ + suppressWhenValuesPrinted?: boolean; +} + +export interface ThemeDataLabels { + show?: 'always' | 'whenTheyFit' | 'never'; + placement?: 'atMark' | 'outsideMark' | 'column'; + inkMode?: 'fixed' | 'matchSeries' | 'contrastWithMark'; +} + +export interface ThemeAnnotation { + unit?: 'never' | 'firstTick' | 'lastTick' | 'firstAndLast' | 'everyTick'; + /** + * `whenAmbiguous` asks the same question of each axis: `Jan Feb Mar` names + * its own kind and needs no title over it, `26 20 14` names nothing until + * one is written. Ranks and binned ranges count as numbers. + */ + axisTitles?: 'omit' | 'whenAmbiguous' | 'always'; + axisTitlePlacement?: 'rotated' | 'flatAboveAxis' | 'inline'; + unitsInAxisTitle?: boolean; + numberFormat?: { + precision?: 'auto' | 'integer' | 'one' | 'two'; + signed?: boolean; + thousands?: 'none' | 'separator' | 'suffix'; + ordinal?: boolean; + }; + pointEmphasis?: 'never' | 'endpoints' | 'latest' | 'extremes'; + pointLabels?: 'never' | 'endpoints' | 'all'; + statistics?: { show?: string[]; placement?: 'panel' | 'caption' }; +} + +export interface ThemeFurniture { + kind: 'mastheadTab' | 'footerRule' | 'headerRule'; + anchor?: 'topLeft' | 'topRight' | 'bottomLeft' | 'bottomRight'; + color?: string; + width?: number; + height?: number; +} + +export interface ThemeFacets { + header?: { presence?: Presence; style?: 'flushLabel' | 'boxedLabel'; fieldTitle?: 'omit' | 'always' }; + panelFrame?: Presence; + axisRepetition?: 'everyPanel' | 'edgeOnly'; + spacing?: 'compact' | 'normal' | 'airy'; + preferredColumns?: number; + sharedScale?: 'always' | 'whenComparable' | 'never'; +} + +export interface ThemeLayout { + density?: 'compact' | 'normal' | 'airy'; + targetWidth?: number; + titleBlock?: { anchor?: 'start' | 'middle' | 'end' }; + bandStep?: number; +} + +/** A predicate over signals the compiler already resolves. Deliberately closed. */ +export interface ThemeGuard { + markChannel?: 'length' | 'position' | 'area' | 'angle' | 'color' | 'text'; + hasBandedAxis?: boolean; + seriesCount?: NumericGuard; + categoryCount?: NumericGuard; + isPartToWhole?: boolean; + isSigned?: boolean; + isTemporal?: boolean; + isFaceted?: boolean; + isSummarised?: boolean; + canvasWidth?: NumericGuard; +} + +export interface NumericGuard { + lt?: number; lte?: number; gt?: number; gte?: number; eq?: number; +} + +export interface ThemeVariant { + when: ThemeGuard; + /** Policy blocks only — `ink` and `type` may not vary. */ + then: Partial>; + /** Required: a variant without a stated reason is an inconsistency. */ + because?: string; +} + +/** + * The one block that is allowed to name a chart type. + * + * Everything else at level 1 is a policy the compiler binds to whatever chart + * it is handed. This is different in kind: it is the house's list of settings + * for charts it has an opinion about — the Times puts a dot on every reading + * of a line, and a bump chart it prints is never smoothed. Those are not + * consequences of a design language, they are house rules, and there is no + * honest way to derive them from ink and type. + * + * Keyed by chart type id, or `*` for every chart. Values are chart-property + * keys the template already declares. It is a *default*: anything the caller + * set explicitly wins, and a key the template does not offer is reported and + * dropped. + */ +export interface ThemeChartDefaults { + [chartType: string]: Record; +} + +/** + * Compiler settings the house prefers — the size it draws at, how far a chart + * may stretch, how much air a facet gets. These are not style: they decide how + * much room the chart has before a single colour is chosen, which is why they + * cannot be applied after the fact like ink. + * + * Three levels, in order: what the caller put in the chart spec, then this, + * then flint's own defaults. A house sets the middle one. + */ +export interface ThemeCompileDefaults extends Partial { + baseSize?: { width: number; height: number }; + canvasSize?: { width: number; height: number }; +} + +/** Level 1. One JSON document per design language. */ +export interface ThemeSpec { + id: string; + label?: string; + ink: ThemeInk; + type: ThemeType; + structure?: ThemeStructure; + marks?: ThemeMarks; + labels?: ThemeLabels; + legend?: ThemeLegend; + dataLabels?: ThemeDataLabels; + annotation?: ThemeAnnotation; + furniture?: ThemeFurniture[]; + facets?: ThemeFacets; + layout?: ThemeLayout; + chartDefaults?: ThemeChartDefaults; + compileDefaults?: ThemeCompileDefaults; + interaction?: { tooltipFormat?: string }; + variants?: ThemeVariant[]; +} + +/** + * A house Flint ships, ready to name by id: `theme_spec: 'economist'`. + * + * Three parts, and they answer different questions. `spec` is what the + * compiler reads. `description` is how a caller chooses between houses. + * `guidance` is the house talking upstream. + * + * That last one needs saying, because the boundary is easy to blur. A theme + * governs the visual: ink, type, furniture, spacing. It does not choose the + * fields, the aggregation or the sort — the chart spec does, and it is written + * first. So where a house depends on something only the spec can give, it says + * so: which words it needs written, which annotations it reads, how many + * categories its colour can name. Facts and requests, not instructions — what + * to do about a tail of thirty categories is the author's call, and a house + * that starts prescribing transformations is overreaching. Hence a few lines. + */ +export interface ThemePreset { + id: string; + label: string; + /** One line: what this house is for. */ + description: string; + /** A few markdown bullets: what this house needs the chart spec to do. */ + guidance: string; + spec: ThemeSpec; +} + +// --------------------------------------------------------------------------- +// Level 2 — grounded DesignDecisions +// --------------------------------------------------------------------------- + +/** + * A downgrade or approximation. Silent fallbacks are indistinguishable from + * bugs, so every one of these is surfaced on `spec._theme.report`. + */ +export interface ThemeReport { + stage: 'ground' | 'realize'; + /** Dotted ThemeSpec path this concerns, e.g. `legend.placement`. */ + path: string; + message: string; +} + +export interface ResolvedText { + font?: string; + fontSize?: number; + fontWeight?: 'normal' | 'bold' | number; + fontStyle?: 'normal' | 'italic'; + color?: string; +} + +export interface ResolvedRule { + show: boolean; + color: string; + width: number; + dash?: number[]; +} + +/** One axis, already bound to a screen channel. */ +export interface ResolvedAxis { + role: 'categorical' | 'measure'; + /** `top`/`bottom` for x, `left`/`right` for y. */ + orient: 'top' | 'bottom' | 'left' | 'right'; + domain: ResolvedRule; + ticks: ResolvedRule & { size: number; offset: number }; + grid: ResolvedRule; + label: ResolvedText & { show?: boolean; limit?: number; padding: number; flush?: boolean; angle?: number }; + title: { show: boolean; placement?: 'rotated' | 'flatAboveAxis' | 'inline'; unit?: string } & ResolvedText; + /** Preferred tick count; undefined = let the renderer choose. */ + tickCount?: number; + /** + * Which ticks carry a label. `all` leaves the choice to the renderer's own + * scale; the rest ask for the values the data actually holds, thinned or + * cut to the two ends. + */ + tickLabels?: 'all' | 'observed' | 'endpoints' | 'sparse'; + /** True when this axis carries what the reader indexes the chart by. */ + indexing?: boolean; + /** A rule at zero, drawn only where the value axis crosses it. */ + zeroRule?: ResolvedRule; + /** Suffix/prefix policy for the measure this axis carries. */ + unit?: { text: string; where: 'never' | 'firstTick' | 'lastTick' | 'firstAndLast' | 'everyTick' }; +} + +export interface ResolvedSeriesInk { + mode: 'single' | 'categorical' | 'sequential' | 'diverging' | 'status'; + single: string; + categorical: string[]; + overflow?: string; + /** + * The data needs more inks than the house named, and the house named no + * overflow ink either. Colour can no longer tell the series apart, so the + * house set is not imposed — what is on the chart already was chosen for + * the count. + */ + exhausted?: boolean; + ramp?: Ramp; + status?: { positive?: string; negative?: string; neutral?: string }; + /** Concrete range to hand a continuous colour scale (already sampled). */ + range?: string[]; + /** Set when the ramp is consumed as discrete bands. */ + quantize?: number; +} + +export interface ResolvedLegend { + show: boolean; + /** The placement that actually survived grounding. */ + placement: LegendPlacement; + /** + * The rest of the house's ranked list, after the one that survived. + * + * Grounding cannot see everything: whether a name fits inside the band it + * names is a question of pixels and text, and it is answered in realize. + * When the answer comes back no, the house has already said what it would + * rather have — so the fallback is read from here rather than invented. + */ + fallbacks?: LegendPlacement[]; + orient?: 'top' | 'right' | 'bottom' | 'left' | 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' | 'none'; + direction?: 'horizontal' | 'vertical'; + title: boolean; + label: ResolvedText; + gradientLength?: number; + /** The most entries a key to values may spend. */ + maxSwatches?: number; +} + +export interface ResolvedDataLabels { + show: boolean; + placement: 'atMark' | 'outsideMark' | 'column'; + inkMode: 'fixed' | 'matchSeries' | 'contrastWithMark'; + text: ResolvedText; + /** d3-format string derived from `annotation.numberFormat` + channel semantics. */ + format?: string; + /** + * The unit each printed value carries. Set only where the house asks for + * a unit and no axis is left to state it — a pie has no ruler at all. + */ + unit?: string; + /** + * Below this magnitude the mark is shorter than its own label, so an + * inside label would overrun it. Grounding owns this because it is a + * question about space, not about style. + */ + insideMinValue?: number; + /** + * Above this magnitude the mark reaches the end of the scale, so an + * outside label would fall off the plot. The mirror of `insideMinValue`. + */ + outsideMaxValue?: number; +} + +export interface ResolvedMarks { + bandFraction: number; + strokeWidth: number; + strokeCap?: string; + strokeJoin?: string; + interpolate?: string; + fillOpacity?: number; + point?: { show: boolean; size?: number; filled?: boolean; haloColor?: string; haloWidth?: number }; + /** The area a sized mark may take, smallest to largest, in px². */ + sizeRange?: [number, number]; + /** The area below which a sized mark stops being a mark. */ + minSize?: number; + /** Whether the rows behind a summary mark are drawn alongside it. */ + observations?: { expose: 'never' | 'whenSparse' | 'always'; maxRows: number }; + separator?: { show: boolean; color: string; width: number }; + /** How far apart neighbouring wedges of a pie or donut stand, in px. */ + slice?: { gap: number; style: 'rule' | 'pad'; color: string }; + /** How far apart the cells of a heatmap or matrix stand, in px. */ + tile?: { gap: number; color: string }; + /** + * `width` is the stem — a connector to the baseline. `spanWidth` is the + * bridge — a connector between two marks, which draws a distance that is + * itself the reading. Both are painted in `color`. + */ + connector?: { show: boolean; color?: string; width: number; spanWidth: number; dash?: number[] }; + interval?: { fillOpacity?: number; edge: boolean }; + summary?: { fill: boolean; outline: boolean; centralRule: boolean; widthFraction?: number }; + reference?: { show: boolean; width: number; style?: string; label: boolean }; + zOrder: 'summaryOverData' | 'summaryUnderData'; + redundantChannels: Array<'shape' | 'dash' | 'texture' | 'lightness'>; + redundantEncoding: Frequency; + /** + * The redundant channels grounding decided this chart actually gets, after + * weighing `redundantEncoding` against how hard the series are to tell + * apart by colour alone. + */ + redundant: { shape: boolean; dash: boolean }; +} + +/** Level 2 output. Backend-neutral, but every role is bound to this chart. */ +export interface DesignDecisions { + themeId: string; + surface: { canvas: string; plot?: string; panel?: string }; + /** Default text ink, for anything not otherwise specified. */ + text: { primary: string; secondary: string; muted: string; inverse: string }; + /** Base font family for the chart body. */ + font?: string; + title: { + anchor: 'start' | 'middle' | 'end'; + headline: ResolvedText; + deck: ResolvedText; + }; + /** Bound axes, keyed by screen channel. */ + axes: { x?: ResolvedAxis; y?: ResolvedAxis }; + frame: ResolvedRule; + baseline: ResolvedRule; + series: ResolvedSeriesInk; + legend: ResolvedLegend; + dataLabels: ResolvedDataLabels; + /** + * Which points on a line the house dots, and whether it writes the value + * there. Only meaningful where the chart draws a line through its data — + * stage 3 knows that, stage 2 does not. + */ + pointEmphasis?: { + where: 'endpoints' | 'latest' | 'extremes'; + labels: 'never' | 'endpoints' | 'all'; + size: number; + }; + marks: ResolvedMarks; + facets: { + header: { show: boolean; fieldTitle: boolean } & ResolvedText; + panelFrame: boolean; + axisRepetition: 'everyPanel' | 'edgeOnly'; + spacing?: number; + preferredColumns?: number; + }; + layout: { padding: number; density: 'compact' | 'normal' | 'airy' }; + /** + * What the house prints alongside a fit: the quantities it expects to see + * stated, and where. Only meaningful where the chart actually fits + * something — stage 3 knows that, stage 2 does not. + */ + statistics?: { show: string[]; placement: 'panel' | 'caption' } & ResolvedText; + furniture: ThemeFurniture[]; + /** Chart facts stage 3 is allowed to consult (it may not re-derive them). */ + bound: { + measureChannels: Array<'x' | 'y'>; + categoricalChannel?: 'x' | 'y'; + seriesChannel?: string; + /** The field the series is keyed on, and the one the categorical axis names. */ + seriesField?: string; + categoryField?: string; + seriesCount: number; + categoryCount: number; + isFaceted: boolean; + isPartToWhole: boolean; + isSigned: boolean; + /** The mark family, for realizers that must fake a missing primitive. */ + markChannel: string; + }; + report: ThemeReport[]; +} diff --git a/packages/flint-js/src/core/types.ts b/packages/flint-js/src/core/types.ts index 4e6db304..a21c997d 100644 --- a/packages/flint-js/src/core/types.ts +++ b/packages/flint-js/src/core/types.ts @@ -5,6 +5,7 @@ import type { ZeroDecision, ColorSchemeRecommendation } from './semantic-types'; import type { LabelSizingDecision } from './decisions'; import type { SemanticAnnotation, FormatSpec, DomainConstraint, TickConstraint } from './field-semantics'; import type { ColorDecisionResult } from './color-decisions'; +import type { ThemeSpec } from './theme/types'; /** * Core types for the chart engine library. @@ -1061,6 +1062,19 @@ export interface ChartAssemblyInput { chart_spec: { /** Template name, e.g. `"Scatter Plot"`, `"Bar Chart"` */ chartType: string; + /** + * The headline — what this chart says, in words. + * + * Not decoration. A chart of bare numbers names nothing on its own, and + * the headline is where the measure gets named: `Male` and `75+` say + * what they are, `35 30 25` does not. Design languages that drop axis + * titles are leaning on this line to carry the subject, so a chart + * authored without one loses the naming altogether — the compiler + * notices, and puts the axis titles back. + */ + title?: string; + /** The deck: the reading of the headline — what is measured, of whom, when, in what units. */ + subtitle?: string; /** Channel → encoding map (e.g., `{ x: { field: 'weight' }, y: { field: 'mpg' } }`). * A bare string is shorthand for `{ field: }` (e.g. `{ x: 'weight' }`). */ encodings: Record; @@ -1089,6 +1103,24 @@ export interface ChartAssemblyInput { chartProperties?: Record; }; + /** + * Theme — describes *how it should look*. + * + * Either the name of a house Flint ships (`'economist'`, `'nature'`, …see + * `listThemePresets()`) or a `ThemeSpec` of your own: a portable design + * language (ink, type, structure, marks, chrome policy), stated without + * ever naming a chart type, a channel, a mark type, a field, or a backend + * property. The compiler grounds it against this chart and then realizes + * it in the target backend. + * + * Sits beside `chart_spec` rather than inside it because the same theme + * applies to every chart and the same chart accepts any theme — nesting it + * would make that independence unstatable. + * + * @experimental Vega-Lite only. + */ + theme_spec?: ThemeSpec | string; + /** * Options for the assembler — layout tuning, tooltips, etc. * All fields are optional and have sensible defaults. diff --git a/packages/flint-js/src/vegalite/assemble.ts b/packages/flint-js/src/vegalite/assemble.ts index 69e6ac0c..63b04fc7 100644 --- a/packages/flint-js/src/vegalite/assemble.ts +++ b/packages/flint-js/src/vegalite/assemble.ts @@ -66,6 +66,9 @@ import { computeLayout, computeChannelBudgets, computeMinSubplotDimensions, deri import { vlApplyLayoutToSpec, vlApplyTooltips } from './instantiate-spec'; import { normalizeStaticSeries } from '../core/static-series'; import { normalizeChartProperties } from '../core/normalize-properties'; +import { groundTheme, resolveChartDefaults, resolveCompileDefaults } from '../core/theme/ground'; +import { resolveThemeSpec } from '../core/theme/presets'; +import { realizeThemeVegaLite, collectMarkTypes, collectPositional } from './theme'; // --------------------------------------------------------------------------- // Helpers @@ -109,14 +112,21 @@ const escapeVlFieldName = (name: string): string => export function assembleVegaLite(input: ChartAssemblyInput): any { const chartType = input.chart_spec.chartType; const semanticTypes = input.semantic_types ?? {}; + // `theme_spec` may name a house Flint ships rather than spell one out. + const themeSpec = resolveThemeSpec(input.theme_spec); + // A house may prefer a size, a stretch budget, a facet gap. Those settle + // before anything is measured, and in a fixed order: the chart spec first, + // the theme's presets under it, flint's own defaults under that. + const themePresets = resolveCompileDefaults(themeSpec, input.options); // Internal layout targets the base (target) size; the optional canvasSize // ceiling is applied as per-dimension stretch caps once options resolve. // The base is clamped to the ceiling so a smaller canvasSize shrinks the // chart to fit rather than overflowing it. - const sizeCeiling = input.chart_spec.canvasSize; - const baseSize = resolveBaseSize(input.chart_spec.baseSize, sizeCeiling); + const housePresets = themeSpec?.compileDefaults; + const sizeCeiling = input.chart_spec.canvasSize ?? housePresets?.canvasSize; + const baseSize = resolveBaseSize(input.chart_spec.baseSize ?? housePresets?.baseSize, sizeCeiling); const canvasSize = baseSize; - const options = input.options ?? {}; + const options = themePresets.options ?? {}; let chartTemplate = vlGetTemplateDef(chartType) as ChartTemplateDef; if (!chartTemplate) { throw new Error(`Unknown chart type: ${chartType}`); @@ -131,9 +141,21 @@ export function assembleVegaLite(input: ChartAssemblyInput): any { const normalizedProps = normalizeChartProperties( chartTemplate.properties, input.chart_spec.chartProperties, ); - const chartProperties = normalizedProps.chartProperties; + let chartProperties = normalizedProps.chartProperties; warnings.push(...normalizedProps.warnings); + // A house's rules about the chart itself — points on a line, a bump chart + // left unsmoothed — change what is drawn, not how it is dressed, so they + // are folded in here, before the pipeline reads the properties. Anything + // the caller stated already is left alone. + if (themeSpec?.chartDefaults && !chartProperties) chartProperties = {}; + const chartDefaultsReport = chartProperties + ? resolveChartDefaults( + themeSpec, chartType, chartTemplate.properties, + input.chart_spec.chartProperties, chartProperties, + ) + : []; + // ═══════════════════════════════════════════════════════════════════════ // PRE-PHASE: Static Series Normalization // ═══════════════════════════════════════════════════════════════════════ @@ -347,6 +369,35 @@ export function assembleVegaLite(input: ChartAssemblyInput): any { ...(declaration.paramOverrides || {}), }; + // How much room one category gets is a house decision — a journal that + // prints three wide boxes and a dashboard that prints thirty thin bars are + // both right about their own page. A template's band size is a guess made + // without knowing the house, so the house's number replaces it; a caller + // who states one outranks both. + // + // But a band step is a statement about bar thickness, and where *both* axes + // are banded there are no bars: the marks are cells, and a cell's size is + // fixed by the two counts and the room they share. A house that asks for + // 80px categories would print a grid of stripes. That one the layout keeps. + const houseBandStep = themeSpec?.layout?.bandStep; + const cellGrid = declaration.axisFlags?.x?.banded === true + && declaration.axisFlags?.y?.banded === true; + if (houseBandStep && options.defaultBandSize == null && !cellGrid) { + effectiveOptions.defaultBandSize = houseBandStep; + effectiveOptions.maxBandSize = Math.max(houseBandStep, effectiveOptions.maxBandSize ?? 0); + chartDefaultsReport.push({ + stage: 'ground', + path: 'layout.bandStep', + message: `the house gives each category ${houseBandStep}px`, + }); + } else if (houseBandStep && cellGrid) { + chartDefaultsReport.push({ + stage: 'ground', + path: 'layout.bandStep', + message: `the house asks for ${houseBandStep}px categories, but both axes are banded — the marks are cells, whose size the grid settles, not the house`, + }); + } + const { addTooltips: addTooltipsOpt = false, minSubplotSize: minSubplotVal = 60, @@ -658,11 +709,69 @@ export function assembleVegaLite(input: ChartAssemblyInput): any { vlApplyTooltips(vgObj); } + // ═══════════════════════════════════════════════════════════════════════ + // HEADLINE + // ═══════════════════════════════════════════════════════════════════════ + // + // Written before theming, because whether the chart has a headline is a + // fact the theme reasons about: a house that omits axis titles is leaning + // on this line to name the measure. + const headline = input.chart_spec.title?.trim(); + const deck = input.chart_spec.subtitle?.trim(); + if (headline || deck) { + vgObj.title = { + text: headline ?? '', + ...(deck ? { subtitle: [deck] } : {}), + }; + } + + // ═══════════════════════════════════════════════════════════════════════ + // THEME (level 2 grounding → level 3 realization) + // ═══════════════════════════════════════════════════════════════════════ + // + // Runs last, deliberately. `vlApplyLayoutToSpec` builds `config` wholesale + // from fit decisions; the theme is a style layer over a chart that already + // fits, so it must see the finished spec. + let themeDecisions: any; + if (themeSpec) { + const markTypes = collectMarkTypes(vgObj); + const stackedChannel = (vgObj.spec?.encoding ?? vgObj.encoding ?? {}); + const stacked = stackedChannel.y?.stack ?? stackedChannel.x?.stack; + themeDecisions = groundTheme(themeSpec, { + chartType, + markChannel: chartTemplate.markCognitiveChannel, + markTypes, + channelSemantics, + resolvedTypes: declaration.resolvedTypes as Record | undefined, + axisFlags: declaration.axisFlags, + positional: collectPositional(vgObj), + layout: layoutResult, + table: values, + canvasSize, + stacked: stacked === 'normalize' ? 'normalize' : Boolean(stacked), + partToWhole: markTypes.includes('arc'), + titled: Boolean(vgObj.title), + hostSurface: (input.options as any)?.background, + }); + const realizeReport = realizeThemeVegaLite(vgObj, themeDecisions, values); + themeDecisions = { + ...themeDecisions, + report: [...themePresets.report, ...chartDefaultsReport, ...themeDecisions.report, ...realizeReport], + }; + } + // ═══════════════════════════════════════════════════════════════════════ // RESULT // ═══════════════════════════════════════════════════════════════════════ const result: any = { ...vgObj, data: vgObj.data ?? { values } }; + if (themeDecisions) { + result._theme = { + id: themeDecisions.themeId, + report: themeDecisions.report, + decisions: themeDecisions, + }; + } if (warnings.length > 0) { result._warnings = warnings; } diff --git a/packages/flint-js/src/vegalite/templates/bar.ts b/packages/flint-js/src/vegalite/templates/bar.ts index ad761e12..2a3a1288 100644 --- a/packages/flint-js/src/vegalite/templates/bar.ts +++ b/packages/flint-js/src/vegalite/templates/bar.ts @@ -479,8 +479,14 @@ export const heatmapDef: ChartTemplateDef = { && !semanticIsDiverging && !isDivergingHeatmapScheme(existingScheme) && colorEncodingType !== 'nominal'; + // A diverging heatmap has a polarity, and the polarity is a reading of + // the field: warm at the top for an intensity, red at the bottom for a + // loss (see the diverging note in semantic-types). That call has + // already been made upstream, so take the scheme it named rather than + // pinning one here — a hard-coded default lands cold-red on every + // temperature grid we draw. const schemeName = userScheme - || (semanticIsDiverging ? (existingScheme || 'redblue') : undefined) + || (semanticIsDiverging ? (existingScheme || semanticScheme?.scheme || 'redblue') : undefined) || (shouldUseHeatmapDefault ? DEFAULT_HEATMAP_SCHEME : existingScheme); const isDiverging = isDivergingHeatmapScheme(schemeName); const intrinsicDomain = getSafeHeatmapIntrinsicDomain(ctx, colorField); @@ -493,12 +499,20 @@ export const heatmapDef: ChartTemplateDef = { if (schemeName) { spec.encoding.color.scale.scheme = schemeName; } - if (isDiverging && effectiveMin < 0 && effectiveMax > 0) { - const sym = Math.max(Math.abs(effectiveMin), Math.abs(effectiveMax)); - effectiveMin = -sym; - effectiveMax = sym; - spec.encoding.color.scale.domain = [-sym, sym]; - spec.encoding.color.scale.domainMid = 0; + // A diverging grid has to be symmetric about its pivot, or one arm + // of the ramp reaches further than the other and equal distances + // from the pivot read as unequal. The pivot is not always zero — + // an author can say what the reader is comparing against — so + // centre on whatever was resolved rather than on the origin. + const pivot = spec.encoding.color.scale.domainMid + ?? semanticScheme?.domainMid + ?? 0; + if (isDiverging && effectiveMin < pivot && effectiveMax > pivot) { + const sym = Math.max(pivot - effectiveMin, effectiveMax - pivot); + effectiveMin = pivot - sym; + effectiveMax = pivot + sym; + spec.encoding.color.scale.domain = [effectiveMin, effectiveMax]; + spec.encoding.color.scale.domainMid = pivot; } else if (intrinsicDomain) { // Sequential color with a known intrinsic domain (e.g. a // Percentage field with [0, 100]). Don't force the full diff --git a/packages/flint-js/src/vegalite/templates/bump.ts b/packages/flint-js/src/vegalite/templates/bump.ts index f84cd9ae..351dfe09 100644 --- a/packages/flint-js/src/vegalite/templates/bump.ts +++ b/packages/flint-js/src/vegalite/templates/bump.ts @@ -3,6 +3,7 @@ import { ChartTemplateDef } from '../../core/types'; import { defaultBuildEncodings } from './utils'; +import { interpolateConfigProperty, applyInterpolate } from './line'; /** Semantic types that indicate a rank-like field */ const RANK_SEMANTIC_TYPES = new Set(['Rank', 'Score', 'Level']); @@ -13,17 +14,25 @@ const isDiscrete = (type: string | undefined) => export const bumpChartDef: ChartTemplateDef = { chart: "Bump Chart", template: { - mark: { type: "line", point: true, interpolate: "monotone", strokeWidth: 2 }, + mark: { type: "line", point: true, interpolate: "linear", strokeWidth: 2 }, encoding: {}, }, channels: ["x", "y", "color", "detail", "column", "row"], markCognitiveChannel: 'position', + properties: [interpolateConfigProperty], declareLayoutMode: () => ({ paramOverrides: { continuousMarkCrossSection: { x: 80, y: 20, seriesCountAxis: 'auto' }, facetAspectRatioResistance: 0.4 }, }), instantiate: (spec, ctx) => { defaultBuildEncodings(spec, ctx.resolvedEncodings); + // Straight segments between one standing and the next. A curve would + // draw a rank the reader can point at halfway between two Games, and + // there was no such rank — nothing was measured between them. A caller + // or a house that wants the softer read says so through the curve + // option. + spec.mark = applyInterpolate(spec.mark, ctx.chartProperties); + const xEnc = spec.encoding?.x; const yEnc = spec.encoding?.y; if (!xEnc || !yEnc) return; @@ -62,5 +71,79 @@ export const bumpChartDef: ChartTemplateDef = { type: yEnc.type || "quantitative", }; } + + applyRankScale(spec.encoding[rankAxis], ctx, rankAxis); + padSequenceEnds(spec.encoding[rankAxis === 'y' ? 'x' : 'y']); }, }; + +/** + * A rank runs from first to last, and there is no zeroth place. + * + * The axis a bump chart is read against is a standing, whatever the field was + * tagged as — the template picked it out for exactly that reason. So it is + * fitted to the standings that exist: an author's declared bounds if there are + * any, otherwise the ranks in the data. A zero baseline here is not a + * conservative choice, it is a tick for a position nobody can finish in, and + * it costs the chart a fifth of its height. + * + * Both ends then get a little air. First place drawn on the frame reads as + * clipped rather than as first, and the label riding on it has nowhere to sit. + */ +function applyRankScale(enc: any, ctx: any, rankAxis: 'x' | 'y'): void { + if (!enc?.field || enc.type !== 'quantitative') return; + + const declared = ctx.channelSemantics?.[rankAxis]?.semanticAnnotation?.intrinsicDomain; + let domain: [number, number] | undefined = Array.isArray(declared) && declared.length === 2 + ? [declared[0], declared[1]] + : undefined; + + if (!domain) { + let min = Infinity; + let max = -Infinity; + for (const row of ctx.table ?? []) { + const v = Number(row?.[enc.field]); + if (!Number.isFinite(v)) continue; + if (v < min) min = v; + if (v > max) max = v; + } + if (!Number.isFinite(min) || !Number.isFinite(max) || min === max) return; + domain = [min, max]; + } + + enc.scale = { ...enc.scale, domain, zero: false, nice: false, padding: RANK_END_PAD }; + + // Every place gets its own tick while there are few enough of them to + // name. A generic tick count on a rank axis lands on 2, 4, 6 and leaves + // first place — the one line the reader came for — with no label at all. + const [lo, hi] = domain; + if (Number.isInteger(lo) && Number.isInteger(hi) && hi - lo <= MAX_NAMED_RANKS) { + const values: number[] = []; + for (let v = lo; v <= hi; v++) values.push(v); + // The count travels with the values: a house that asks for four ticks + // on its value axes means four on a measured axis, but here the ticks + // are the places themselves, and Vega thins a named list down to the + // house count unless it is told how many names there are. + enc.axis = { ...enc.axis, values, tickCount: values.length }; + } +} + +/** Ranks we will label one by one before falling back to the axis's own count. */ +const MAX_NAMED_RANKS = 11; + +/** Room at the ends of the scale, in pixels. */ +const RANK_END_PAD = 14; +const SEQUENCE_END_PAD = 10; + +/** + * Air at the start and the end of the sequence axis. + * + * The first reading sits on the value axis and the last sits on the frame, + * which puts a point and its name in the same pixels as the axis labels. The + * padding is what the reader would leave if they were drawing it by hand. + */ +function padSequenceEnds(enc: any): void { + if (!enc?.field || enc.type === 'nominal' || enc.type === 'ordinal') return; + if (enc.scale?.padding != null) return; + enc.scale = { ...enc.scale, padding: SEQUENCE_END_PAD }; +} diff --git a/packages/flint-js/src/vegalite/templates/slope.ts b/packages/flint-js/src/vegalite/templates/slope.ts index bfd714ac..15fe1e3c 100644 --- a/packages/flint-js/src/vegalite/templates/slope.ts +++ b/packages/flint-js/src/vegalite/templates/slope.ts @@ -25,7 +25,7 @@ * shows points at both ends. */ -import { ChartTemplateDef } from '../../core/types'; +import { ChartTemplateDef, ChartPropertyDef } from '../../core/types'; import { resolveDiscreteType } from '../../core/axis-detection'; import { defaultBuildEncodings } from './utils'; @@ -114,7 +114,9 @@ export const slopeChartDef: ChartTemplateDef = { // Inset the two period bands from the plot edges so the end points and // their value labels are not clipped (classic slopegraph framing). - xEnc.scale = { ...xEnc.scale, padding: 0.4 }; + const labelled = ctx.chartProperties?.showText === true; + const named = labelled && ctx.chartProperties?.showSeriesInLabel === true; + xEnc.scale = { ...xEnc.scale, padding: named ? 0.75 : labelled ? 0.55 : 0.4 }; // Give the value axis a little breathing room (in pixels) so the // extreme top / bottom end-point markers — common with zero-crossing @@ -123,5 +125,67 @@ export const slopeChartDef: ChartTemplateDef = { // data rather than anchoring at zero — matching the ECharts / Chart.js // slope templates and classic slopegraph convention. yEnc.scale = { ...yEnc.scale, zero: false, nice: true, padding: 12 }; + + // A slopegraph is a table that happens to be drawn. Its two columns of + // numbers are the point of it — the line only says which way the pair + // moved — so printing the values at both ends is the chart's own job, + // not an annotation added over it. And a value with no name attached is + // half a row: `showSeriesInLabel` puts the two back together and makes + // the colour legend redundant. + const props = ctx.chartProperties; + if (props?.showText === true) { + const periods = orderedDistinct(ctx.table, xEnc.field); + if (periods.length >= 2) { + const first = periods[0]; + const last = periods[periods.length - 1]; + const seriesField = ctx.channelSemantics?.color?.field + ?? ctx.channelSemantics?.detail?.field; + const withSeries = props.showSeriesInLabel === true && !!seriesField; + const fmt = props.labelFormat ?? '.3~s'; + const valueExpr = `format(datum[${JSON.stringify(yEnc.field)}], ${JSON.stringify(fmt)})`; + const labelExpr = withSeries + ? `datum[${JSON.stringify(seriesField)}] + ' ' + ${valueExpr}` + : valueExpr; + const endLayer = (period: unknown, align: 'left' | 'right') => ({ + transform: [ + { filter: { field: xEnc.field, equal: period as any } }, + { calculate: labelExpr, as: '__slopeLabel' }, + ], + mark: { + type: 'text', + align, + baseline: 'middle', + dx: align === 'right' ? -8 : 8, + fontSize: 11, + }, + encoding: { + ...JSON.parse(JSON.stringify(spec.encoding)), + text: { field: '__slopeLabel', type: 'nominal' }, + }, + }); + const lineLayer: Record = { mark: spec.mark, encoding: spec.encoding }; + if (spec.transform) lineLayer.transform = spec.transform; + spec.layer = [ + lineLayer, + endLayer(first, 'right'), + endLayer(last, 'left'), + ]; + delete spec.mark; + delete spec.encoding; + delete spec.transform; + } + } }, + properties: [ + { + key: 'showText', label: 'Values', type: 'binary', defaultValue: false, + }, + { + key: 'showSeriesInLabel', label: 'Name in label', type: 'binary', defaultValue: false, + // A name only goes in the label when there is a name to put there. + check: (ctx) => ({ + applicable: Boolean(ctx.encodings?.color?.field || ctx.encodings?.detail?.field), + }), + }, + ] as ChartPropertyDef[], }; diff --git a/packages/flint-js/src/vegalite/templates/violin.ts b/packages/flint-js/src/vegalite/templates/violin.ts index 8c1d49b2..dad02846 100644 --- a/packages/flint-js/src/vegalite/templates/violin.ts +++ b/packages/flint-js/src/vegalite/templates/violin.ts @@ -302,8 +302,175 @@ export const violinPlotDef: ChartTemplateDef = { spec.width = panelW; spec.height = panelH; } + + // --- What the smoothing hides ------------------------------------ + // A kernel density is an *inference*: it draws a curve where there + // were points, and with twelve birds per species the curve says more + // than the sample can support. Houses that publish distributions in + // print answer that by drawing the evidence next to the estimate — + // every observation, and the one summary the eye cannot read off a + // smooth shape, the median. + const wantPoints = config?.showPoints === true; + const wantMedian = config?.showMedian === true; + // A density estimate has an edge, and whether that edge is drawn is a + // real choice: a wash with no contour reads as a cloud, a contour reads + // as a measured shape. Houses that print distributions draw the line. + const wantContour = config?.showContour === true; + const medianWidth = typeof config?.medianWidth === 'number' && config.medianWidth > 0 + ? Math.min(1, config.medianWidth) + : 0.6; + if (wantPoints || wantMedian || wantContour) { + // A centre stack does not centre on zero — it centres every shape + // on half the widest density in the panel set, so zero sits at the + // left edge and anything drawn at zero misses the violin. The + // mirror is therefore cut by hand, half the density either side of + // zero, which puts the centre line where the jitter expects it. + const baseX = { ...(spec.encoding.x || {}) }; + delete baseX.stack; + delete baseX.impute; + delete baseX.field; + const layers: any[] = [{ + transform: [ + ...(spec.transform || []), + { calculate: 'datum.density / 2', as: '__violinHalf' }, + { calculate: '-datum.density / 2', as: '__violinNegHalf' }, + ], + // A solid shape drawn over the evidence hides it: the estimate + // steps back to a wash so the observations read through it. + mark: wantPoints + ? { ...(typeof spec.mark === 'string' ? { type: spec.mark } : spec.mark), fillOpacity: 0.35 } + : spec.mark, + encoding: { + y: spec.encoding.y, + x: { ...baseX, field: '__violinHalf', type: 'quantitative', stack: null }, + x2: { field: '__violinNegHalf' }, + }, + }]; + if (wantContour) { + // An area mark fills a shape; it does not draw one, and its + // `line` overlay follows only the leading edge — half a + // silhouette on a mirrored density. So the contour is two + // lines, one down each side, both riding the same colour scale + // as the wash they enclose. `point: false` is not decoration: + // a house that puts a dot on every line vertex would otherwise + // bead two hundred kernel samples along the outline. + for (const edge of ['__violinHalf', '__violinNegHalf']) { + layers.push({ + transform: [ + ...(spec.transform || []), + { calculate: 'datum.density / 2', as: '__violinHalf' }, + { calculate: '-datum.density / 2', as: '__violinNegHalf' }, + ], + mark: { + type: 'line', orient: 'horizontal', strokeWidth: 1, + opacity: 0.9, point: false, + }, + encoding: { + y: spec.encoding.y, + x: { ...baseX, field: edge, type: 'quantitative', stack: null }, + }, + }); + } + } + if (wantPoints) { + // A normal kernel peaks at ~0.4 / bandwidth, and the mirror + // seats half of that on each side of the centre line, so a + // quarter of the peak is a strip of jitter that stays well + // inside the shape it belongs to — and it is measured in the + // same density units, so it rides the violin's own scale. + const peak = effectiveBw > 0 ? 0.4 / effectiveBw : 0; + const jitter = peak * 0.25; + layers.push({ + transform: [{ + calculate: jitter > 0 ? `(random() - 0.5) * ${jitter}` : '0', + as: '__violinJitter', + }], + mark: { type: 'point', filled: true, size: 16, opacity: 0.9 }, + encoding: { + y: { + field: measureField, type: 'quantitative', title: measureField, + // The violin is a window on the distribution, not a + // length measured from nothing — a point mark would + // otherwise drag the axis down to zero. + scale: { zero: false }, + }, + x: { + field: '__violinJitter', type: 'quantitative', title: null, + axis: null, stack: null, + }, + }, + }); + } + if (wantMedian) { + // A rule drawn the full width of the panel is not a summary of + // *this* shape, it is a line across the page that happens to + // pass through the median. The mirror puts the widest a violin + // can ever get at half the kernel peak, so the rule is cut to a + // fraction of that — the same width in every panel, always + // inside the widest shape, and visibly a mark on the violin + // rather than a graticule behind it. + const peakHalf = effectiveBw > 0 ? (0.4 / effectiveBw) / 2 : 0; + const half = peakHalf * medianWidth; + layers.push({ + transform: [{ + aggregate: [{ op: 'median', field: measureField, as: '__violinMedian' }], + groupby, + }], + mark: { type: 'rule', strokeWidth: 1.5 }, + encoding: { + y: { + field: '__violinMedian', type: 'quantitative', title: measureField, + scale: { zero: false }, + }, + ...(half > 0 + ? { x: { datum: -half, type: 'quantitative' }, x2: { datum: half } } + : {}), + }, + }); + } + // Vega-Lite ignores a facet *channel* on a layered spec, so the + // per-category panels move to the facet operator: the panels wrap + // the layers instead of sitting beside them. + const enc = spec.encoding; + const wrap = enc.facet; + const shared = { ...enc }; + delete shared.facet; + delete shared.column; + delete shared.row; + // Position belongs to each layer — a shared x would push its stack + // onto the observations and its field onto the median rule. + delete shared.x; + delete shared.y; + const inner: any = { layer: layers, encoding: shared }; + if (spec.width != null) inner.width = spec.width; + if (spec.height != null) inner.height = spec.height; + if (wrap) { + const { columns, ...def } = wrap; + spec.facet = def; + if (columns != null) spec.columns = columns; + } else { + spec.facet = { + ...(enc.column ? { column: enc.column } : {}), + ...(enc.row ? { row: enc.row } : {}), + }; + } + spec.spec = inner; + delete spec.mark; + delete spec.transform; + delete spec.encoding; + delete spec.width; + delete spec.height; + } }, properties: [ { key: 'bandwidth', label: 'Bandwidth', type: 'continuous', min: 0.05, max: 2, step: 0.05, defaultValue: 0 }, + { key: 'showPoints', label: 'Observations', type: 'binary', defaultValue: false }, + { key: 'showMedian', label: 'Median rule', type: 'binary', defaultValue: false }, + { key: 'showContour', label: 'Contour', type: 'binary', defaultValue: false }, + { + key: 'medianWidth', label: 'Median width', type: 'continuous', + min: 0.2, max: 1, step: 0.05, defaultValue: 0.6, + check: (ctx: any) => ({ applicable: ctx.chartProperties?.showMedian === true }), + }, ] as ChartPropertyDef[], }; diff --git a/packages/flint-js/src/vegalite/theme.ts b/packages/flint-js/src/vegalite/theme.ts new file mode 100644 index 00000000..ab064fb5 --- /dev/null +++ b/packages/flint-js/src/vegalite/theme.ts @@ -0,0 +1,3169 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** + * Level 3 — realization, Vega-Lite. + * + * Takes backend-neutral `DesignDecisions` and writes Vega-Lite. This file is + * allowed to *approximate* — to fake a primitive Vega-Lite does not have, such + * as line-end series labels — and is required to report when it does. + * + * It is NOT allowed to decide anything. Any `if (chartType === …)` here is a + * bug in grounding, not a shortcut. + * + * Runs LAST, after `vlApplyLayoutToSpec`, which builds `spec.config` wholesale. + */ + +import type { DesignDecisions, ThemeReport } from '../core/theme/types.js'; +import { contrastingInk, parseColor, luminance, toHex } from '../core/theme/presence.js'; + +/** Mark families that carry data values (as opposed to chrome). */ +const DATA_MARKS = new Set([ + 'bar', 'line', 'area', 'point', 'circle', 'square', 'arc', 'rect', + 'trail', 'tick', 'boxplot', 'errorbar', 'errorband', 'geoshape', +]); + +const LINE_MARKS = new Set(['line', 'trail']); + +/** Axis properties this realizer owns; anything a template left is overwritten. */ +const OWNED_AXIS_KEYS = [ + 'grid', 'gridColor', 'gridWidth', 'gridDash', 'gridOpacity', + 'domain', 'domainColor', 'domainWidth', + 'ticks', 'tickColor', 'tickWidth', 'tickSize', 'tickOffset', + 'labelColor', 'labelFont', 'labelFontSize', 'labelFontWeight', 'labelPadding', + 'titleColor', 'titleFont', 'titleFontSize', 'titleFontWeight', +]; + +export function markTypeOf(mark: any): string | undefined { + if (!mark) return undefined; + return typeof mark === 'string' ? mark : mark.type; +} + +/** Every mark family present anywhere in the spec. Used to ground before realizing. */ +export function collectMarkTypes(spec: any): string[] { + const found = new Set(); + walk(spec, (node) => { + const t = markTypeOf(node.mark); + if (t) found.add(t); + }); + return [...found]; +} + +/** + * What the spec actually put on the two screen axes. A template is free to + * name its semantic channels `high`/`low`/`open`/`close`; the reader still + * sees an x and a y, and grounding has to be able to bind them. + */ +export function collectPositional(spec: any): { x?: { type?: string; field?: string }; y?: { type?: string; field?: string }; color?: { type?: string; field?: string } } { + const out: any = {}; + // The type and the field are collected independently: a layered template + // states the type once on the shared encoding and the field on each layer. + walk(spec, (node) => { + for (const ch of ['x', 'y'] as const) { + const enc = node.encoding?.[ch]; + if (!enc || typeof enc !== 'object') continue; + const cur = out[ch] ?? (out[ch] = {}); + if (cur.type == null && enc.type) cur.type = enc.type; + if (cur.type == null && enc.aggregate) cur.type = 'quantitative'; + if (cur.field == null && enc.field) cur.field = enc.field; + } + // The same for colour, which a layered template states on the one + // layer that needs it. Grounding otherwise concludes "no series" for a + // chart the reader plainly sees two series in. + for (const ch of ['color', 'fill', 'stroke'] as const) { + const enc = node.encoding?.[ch]; + if (!enc?.field || out.color) continue; + out.color = { field: enc.field, type: enc.type }; + } + }); + for (const ch of ['x', 'y'] as const) { + if (out[ch] && out[ch].type == null && out[ch].field == null) delete out[ch]; + } + // Whether the marks are stacked is a fact about the chart too, and one + // grounding cannot get from the semantics: Vega-Lite stacks a bar with a + // colour channel without being asked. + for (const body of plotBodies(spec)) { + const units = body.layer ? body.layer : [body]; + for (const unit of units) { + if (!DATA_MARKS.has(markTypeOf(unit.mark) ?? '')) continue; + const merged = { ...(body.encoding ?? {}), ...(unit.encoding ?? {}) }; + const probe = { mark: unit.mark, encoding: merged }; + if (isStacked(probe, 'x') || isStacked(probe, 'y')) out.stacked = true; + } + } + return out; +} + +function walk(node: any, visit: (n: any) => void): void { + if (!node || typeof node !== 'object') return; + visit(node); + for (const key of ['layer', 'vconcat', 'hconcat', 'concat']) { + if (Array.isArray(node[key])) node[key].forEach((c: any) => walk(c, visit)); + } + if (node.spec) walk(node.spec, visit); + if (node.facet && node.facet.spec) walk(node.facet.spec, visit); +} + +/** + * The node that owns the plot body — where layers must be added and where the + * positional encodings live. For a facet spec that is `spec`, for a concat it + * is the first child that has marks, otherwise the spec itself. + */ +function plotBody(spec: any): any { + let node = spec; + for (let depth = 0; depth < 6; depth++) { + if (node.mark || node.layer) return node; + if (node.spec) { node = node.spec; continue; } + for (const key of ['vconcat', 'hconcat', 'concat']) { + if (Array.isArray(node[key]) && node[key].length) { node = node[key][0]; break; } + } + if (node === spec) break; + } + return node.mark || node.layer ? node : spec; +} + +/** + * Every plot body in the spec. A concatenation has one per panel, and each of + * them is a chart in its own right — the mark-level passes have to visit all + * of them, not just the first. + */ +function plotBodies(spec: any): any[] { + const out: any[] = []; + const visit = (node: any, depth: number): void => { + if (!node || typeof node !== 'object' || depth > 6) return; + if (node.mark || node.layer) { out.push(node); return; } + if (node.spec) return visit(node.spec, depth + 1); + for (const key of ['vconcat', 'hconcat', 'concat'] as const) { + if (Array.isArray(node[key])) node[key].forEach((c: any) => visit(c, depth + 1)); + } + }; + visit(spec, 0); + return out.length ? out : [plotBody(spec)]; +} + +/** Merged encoding visible to a unit: node-level plus its own. */ +function mergedEncoding(node: any, inherited: any): any { + return { ...(inherited ?? {}), ...(node.encoding ?? {}) }; +} + +/** The words Vega-Lite will write on this axis if nobody says otherwise. */ +function titleOf(enc: any): string | undefined { + if (typeof enc?.title === 'string') return enc.title; + return typeof enc?.field === 'string' ? enc.field : undefined; +} + +// --------------------------------------------------------------------------- +// Entry point +// --------------------------------------------------------------------------- + +export function realizeThemeVegaLite(spec: any, d: DesignDecisions, table: any[] = []): ThemeReport[] { + const report: ThemeReport[] = []; + const say = (path: string, message: string) => report.push({ stage: 'realize', path, message }); + + const config = (spec.config ??= {}); + + applySurface(spec, config, d); + applyTypography(config, d); + applyAxes(spec, config, d, table, say); + applyZeroRule(spec, d, table, say); + applyMarks(spec, d, table, say); + applySeriesInk(spec, d, table, say); + applyConnectors(spec, d, say); + applyRedundantChannels(spec, d, say); + demoteSeriesEnd(spec, d, say); + applyLegend(spec, config, d, table, say); + applyFacetChrome(config, d); + applyPanelTitles(spec, d, say); + const valueLayer = applyDataLabels(spec, d, table, say); + applySeriesEndLabels(spec, d, valueLayer, table, say); + applyPointEmphasis(spec, d, say); + applyPrintedUnits(spec, d, say); + applyStatistics(spec, d, table, say); + const wrapped = applyFurniture(spec, d, table, say); + + return wrapped ? report : report; +} + +// --------------------------------------------------------------------------- +// Surface & typography +// --------------------------------------------------------------------------- + +function applySurface(spec: any, config: any, d: DesignDecisions): void { + spec.background = d.surface.canvas; + config.background = d.surface.canvas; + + config.view = { ...(config.view ?? {}) }; + config.view.stroke = d.frame.show ? d.frame.color : null; + if (d.frame.show) config.view.strokeWidth = d.frame.width; + if (d.surface.plot && d.surface.plot !== d.surface.canvas) config.view.fill = d.surface.plot; + + if (spec.padding == null) spec.padding = d.layout.padding; +} + +function applyTypography(config: any, d: DesignDecisions): void { + if (d.font) config.font = d.font; + + const h = d.title.headline; + const deck = d.title.deck; + config.title = { + ...(config.title ?? {}), + font: h.font, + fontSize: h.fontSize, + fontWeight: h.fontWeight ?? 700, + ...(h.fontStyle ? { fontStyle: h.fontStyle } : {}), + color: h.color, + anchor: d.title.anchor, + offset: Math.round((h.fontSize ?? 14) * 0.9), + subtitleFont: deck.font, + subtitleFontSize: deck.fontSize, + ...(deck.fontStyle ? { subtitleFontStyle: deck.fontStyle } : {}), + subtitleColor: deck.color, + subtitlePadding: Math.round((deck.fontSize ?? 11) * 0.6), + }; + // A start-anchored headline belongs at the edge of the *graphic*, not at + // the left edge of the plotting rectangle — otherwise the width of the + // category labels decides where the title starts. + if (d.title.anchor !== 'middle') config.title.frame = 'bounds'; +} + +// --------------------------------------------------------------------------- +// Axes +// --------------------------------------------------------------------------- + +const NONLINEAR_SCALES = new Set(['log', 'symlog', 'pow', 'sqrt']); + +/** + * Flint's preferred tick-label size for this backend (`baseLabelFontSize` in + * the assembler). Anything under it is the layout pass reporting that the + * labels did not fit at the size it wanted. + */ +const BASE_LABEL_FONT_SIZE = 10; + +function applyAxes(spec: any, config: any, d: DesignDecisions, table: any[], say: (p: string, m: string) => void): void { + for (const channel of ['x', 'y'] as const) { + const axis = d.axes[channel]; + if (!axis) continue; + + const key = channel === 'x' ? 'axisX' : 'axisY'; + // A rule under the categories is the line the marks stand on, and a + // chart whose value scale floats does not stand on anything: the + // slopegraph's 40 is not a base, it is wherever the window happened to + // start. Drawing a heavy rule there asserts a zero that is not on the + // page. But this is an argument about *bands*: where the index axis is + // itself a measure — a scatter's horizontal — its rule is the edge of + // the window, not a base anything is measured from, and taking it away + // leaves the plot hanging off a single wall. + let domainShow = axis.domain.show; + if (domainShow && axis.indexing && bandedAxis(spec, channel) && floatingValueScale(spec, channel)) { + domainShow = false; + say(`axes.${channel}.domain`, + 'the value scale floats — a rule under the categories would claim a base the chart does not have'); + } + const themed: any = { + grid: axis.grid.show, + gridColor: axis.grid.color, + gridWidth: axis.grid.width, + domain: domainShow, + domainColor: axis.domain.color, + domainWidth: axis.domain.width, + ticks: axis.ticks.show, + tickColor: axis.ticks.color, + tickWidth: axis.ticks.width, + tickSize: axis.ticks.size, + labelFont: axis.label.font, + labelFontSize: axis.label.fontSize, + labelColor: axis.label.color, + labelPadding: axis.label.padding, + titleFont: axis.title.font, + titleFontSize: axis.title.fontSize, + titleColor: axis.title.color, + titleFontWeight: axis.title.fontWeight ?? 'normal', + }; + if (axis.grid.dash) themed.gridDash = axis.grid.dash; + if (axis.ticks.offset) themed.tickOffset = axis.ticks.offset; + if (axis.label.show === false) themed.labels = false; + if (axis.label.limit != null) themed.labelLimit = axis.label.limit; + if (axis.label.angle != null) themed.labelAngle = axis.label.angle; + if (axis.tickCount != null) themed.tickCount = axis.tickCount; + + // Flint's layout pass owns label rotation/anchoring when the theme has + // no opinion — those are fit decisions, not style ones. + const existing = config[key] ?? {}; + + // How big the tick labels are is a house matter; whether they still + // read is not. Flint's layout shrinks them below its own base only + // when the categories crowd — a hundred and twenty bands in a plot + // six pixels wide apiece — and setting a larger house face on top of + // that decision does not restyle the axis, it smears it. So the house + // size holds up to the size fit allows. + const fitted = existing.labelFontSize; + if (typeof fitted === 'number' && fitted < BASE_LABEL_FONT_SIZE + && typeof themed.labelFontSize === 'number' && themed.labelFontSize > fitted) { + say(`axes.${channel}.label.fontSize`, + `the axis is crowded — the layout fitted its labels at ${fitted}px and the house's ${themed.labelFontSize}px would not stand in the band`); + themed.labelFontSize = fitted; + } + + config[key] = { ...existing, ...themed }; + if (axis.label.angle == null && existing.labelAngle != null) { + config[key].labelAngle = existing.labelAngle; + } + + // Encoding-level axis objects outrank config, so anything a template + // left behind in the keys this file owns has to go. + let saidGrid = false; + let saidTicks = false; + let saidGutter = false; + let saidUnit = false; + let flatTitle = false; + walk(spec, (node) => { + const enc = node.encoding?.[channel]; + if (!enc || enc.axis === null) return; + const ax = enc.axis; + if (ax && typeof ax === 'object') { + // A few pixels of label padding is styling and this file owns + // it. A hundred is not padding, it is a *column*: the template + // reserved a gutter and set the labels flush into it, and + // trimming it back to four drops the names onto the marks. + const gutter = typeof ax.labelPadding === 'number' + && ax.labelPadding >= (axis.label.padding ?? 4) + 16 + ? ax.labelPadding + : undefined; + for (const k of OWNED_AXIS_KEYS) delete ax[k]; + if (gutter != null) { + ax.labelPadding = gutter; + if (!saidGutter) { + say(`axes.${channel}.label.padding`, + `the template holds a ${gutter}px gutter for its labels — that is layout, not padding, so it stands`); + saidGutter = true; + } + } + } + if (!axis.title.show) { + enc.axis = { ...(ax ?? {}), title: null }; + } else { + // The unit belongs in the title where the house keeps titles — + // `Weight (lb)`, not `1500 lb`, `2000 lb`, `2500 lb` — unless + // the field already says it. + if (axis.title.unit) { + const current = enc.axis?.title ?? titleOf(enc); + if (typeof current === 'string' && !current.includes(`(${axis.title.unit})`)) { + enc.axis = { ...(enc.axis ?? {}), title: `${current} (${axis.title.unit})` }; + if (!saidUnit) { + say(`axes.${channel}.title`, + `the unit \`${axis.title.unit}\` is stated once in the axis title, which this house keeps`); + saidUnit = true; + } + } + } + // A title lying flat above its own axis reads as a label, not + // as a caption turned on its side. Only the vertical axis has + // anything to turn. + const placement = axis.title.placement; + if (channel === 'y' && !flatTitle + && (placement === 'flatAboveAxis' || placement === 'inline')) { + enc.axis = { + ...(enc.axis ?? {}), + titleAngle: 0, + titleAlign: 'left', + titleAnchor: placement === 'inline' ? 'end' : 'start', + titleX: 0, + titleY: -(axis.title.fontSize ?? 11) - 6, + titleBaseline: 'bottom', + }; + growPadding(spec, 'top', (axis.title.fontSize ?? 11) + 8); + flatTitle = true; + say(`axes.${channel}.title.placement`, + 'the axis title lies flat above the axis, where it reads as a label rather than a caption on its side'); + } + } + const defaultOrient = channel === 'x' ? 'bottom' : 'left'; + if (axis.orient !== defaultOrient) { + enc.axis = { ...(enc.axis ?? {}), orient: axis.orient }; + } + // A grid line the reader cannot name is not a grid, it is a fence. + // A log scale offers a line at 2, 3, 4… of every decade, and Vega + // prunes the *labels* that collide, not the lines under them. Where + // the house draws a grid on such a scale, the ticks are cut back to + // the decades — the ones that will still carry a number. + if (axis.grid.show && NONLINEAR_SCALES.has(enc.scale?.type) + && (enc.axis?.tickCount ?? enc.axis?.values) == null) { + const decades = decadeTicks(table, enc.field); + if (decades) { + enc.axis = { ...(enc.axis ?? {}), values: decades }; + if (!saidGrid) { + say(`axes.${channel}.grid`, + `the ${enc.scale.type} scale offers a line at every step of every decade — the grid is cut back to the ${decades.length} it can label`); + saidGrid = true; + } + } + } + + // A tick between two observations names a value the chart does not + // hold: an axis of Olympic years does not have a 2014. Where the + // house asks for the values the data carries, the axis is stepped + // by what the data is actually spaced by. + if (axis.indexing && axis.tickLabels && axis.tickLabels !== 'all' + && enc.axis?.values == null && enc.axis?.tickCount == null) { + const size = channel === 'x' ? (node.width ?? spec.width) : (node.height ?? spec.height); + const span = typeof size === 'number' ? size : (channel === 'x' ? 600 : 300); + const fontSize = axis.label.fontSize ?? 10; + if (enc.type === 'temporal') { + // Stating dates outright costs the renderer the format it + // would have inferred — "1960" becomes "04 PM" — so a + // format comes with them. Where the dates are not plain + // calendar dates, an interval and a step say the same + // thing without risking a timezone. + const dates = observedDates(table, enc.field, axis.tickLabels, span, fontSize); + if (dates) { + enc.axis = { ...(enc.axis ?? {}), values: dates.values, format: dates.format }; + // The dates were read as UTC; the scale has to agree, + // or a tick lands hours off its own observation and + // the last one falls outside the domain entirely. + enc.scale = { ...(enc.scale ?? {}), type: 'utc' }; + if (!saidTicks) { + say(`axes.${channel}.tickLabels`, + `${axis.tickLabels} labels — the axis is ticked at ${dates.values.length} of the dates the data holds, not at round numbers between them`); + saidTicks = true; + } + } else { + const every = temporalStep(table, enc.field, span, fontSize); + if (every) { + enc.axis = { ...(enc.axis ?? {}), tickCount: every }; + if (!saidTicks) { + say(`axes.${channel}.tickLabels`, + `${axis.tickLabels} labels — the axis is stepped every ${every.step} ${every.interval}${every.step === 1 ? '' : 's'}, which is how the data is spaced`); + saidTicks = true; + } + } + } + } else if (enc.type === 'quantitative') { + const values = observedTicks(table, enc.field, axis.tickLabels, span, fontSize); + if (values) { + enc.axis = { ...(enc.axis ?? {}), values }; + if (!saidTicks) { + say(`axes.${channel}.tickLabels`, + `${axis.tickLabels} labels — the axis is ticked at the ${values.length} values the data holds, not at round numbers between them`); + saidTicks = true; + } + } + } + } + + // The unit has to be written somewhere. A house that drops axis + // titles has taken away the usual place, so it says where else — + // on every tick, or once at the end of the ruler. + const unit = axis.unit; + if (unit && unit.where !== 'never' && axis.label.show !== false + && (enc.type === 'quantitative' || enc.axis?.values != null)) { + const tagged = tagWithUnit(enc.axis?.labelExpr, unit.text, unit.where); + enc.axis = { ...(enc.axis ?? {}), labelExpr: tagged }; + if (!saidUnit) { + say(`axes.${channel}.unit`, + unit.where === 'everyTick' + ? `every label carries its unit — \`${unit.text}\` — because the house prints no axis title to hold it` + : `the unit \`${unit.text}\` rides on the ${unit.where === 'firstTick' ? 'first' : unit.where === 'firstAndLast' ? 'first and last' : 'last'} label, where the ruler ends`); + saidUnit = true; + } + } + }); + } +} + +/** + * True when the axis holds discrete positions — names, not numbers. Only such + * an axis is a floor the marks stand on; a continuous one is a ruler, and its + * rule is the edge of the window. + */ +function bandedAxis(spec: any, channel: 'x' | 'y'): boolean { + let banded = false; + walk(spec, (node) => { + const enc = node.encoding?.[channel]; + if (!enc?.field) return; + if (enc.type === 'nominal' || enc.type === 'ordinal') banded = true; + }); + return banded; +} + +/** + * True when the *other* axis is a window on the data rather than a scale from + * zero — the template said `zero: false`, or pinned a domain that does not + * reach it. Nothing on such a chart is measured from the axis line. + * + * `zero: false` is not the last word, though: a lollipop turns it off so the + * dots are not squashed against the top, then hangs every stem from + * `datum: 0`. A mark that reaches the base *puts* the base on the page, + * whatever the scale asked for, and the rule under it is the floor those stems + * land on. + */ +function floatingValueScale(spec: any, indexChannel: 'x' | 'y'): boolean { + const other = indexChannel === 'x' ? 'y' : 'x'; + let floating = false; + let seen = false; + let anchored = false; + walk(spec, (node) => { + const enc = node.encoding?.[other]; + const end = node.encoding?.[`${other}2`]; + if (enc?.datum === 0 || end?.datum === 0) anchored = true; + if (!enc?.field || enc.type === 'nominal' || enc.type === 'ordinal') return; + seen = true; + const scale = enc.scale; + if (scale?.zero === false) floating = true; + if (Array.isArray(scale?.domain) && typeof scale.domain[0] === 'number' && scale.domain[0] > 0) floating = true; + }); + return seen && floating && !anchored; +} + +/** + * A rule where the value axis crosses zero. + * + * Zero only earns its own line when it is *inside* the plot: on a chart of + * lengths it sits at the edge and the axis rule already states it, and on a + * scale that never changes sign it is not a crossing but a corner. So the rule + * is drawn only where the field runs both ways. + */ +function applyZeroRule(spec: any, d: DesignDecisions, table: any[], say: (p: string, m: string) => void): void { + let said = false; + for (const channel of ['x', 'y'] as const) { + const axis = d.axes[channel]; + const zero = axis?.zeroRule; + if (!zero?.show) continue; + for (const body of plotBodies(spec)) { + const enc = (body.encoding?.[channel]) ?? body.layer?.[0]?.encoding?.[channel]; + const field = enc?.field; + if (!field || enc.type === 'nominal' || enc.type === 'ordinal') continue; + let min = Infinity; + let max = -Infinity; + for (const row of table ?? []) { + const v = Number(row?.[field]); + if (!Number.isFinite(v)) continue; + if (v < min) min = v; + if (v > max) max = v; + } + if (!(min < 0 && max > 0)) continue; + const rule = { + data: { values: [{}] }, + mark: { + type: 'rule', + color: zero.color, + strokeWidth: zero.width ?? 1, + ...(zero.dash ? { strokeDash: zero.dash } : {}), + }, + encoding: { [channel]: { datum: 0 } }, + }; + if (Array.isArray(body.layer)) body.layer.push(rule); + else { + const own = { mark: body.mark, encoding: body.encoding }; + delete body.mark; + body.layer = [own, rule]; + } + if (!said) { + say('structure.grid.zero', 'the measure changes sign inside the plot — zero is drawn as its own rule, not as one gridline among the rest'); + said = true; + } + } + } +} + +/** + * The decades a field spans, for a log axis whose grid must be nameable. + * Returns `undefined` when the data is not readable from the spec or the span + * is too short to be worth thinning. + */ +function decadeTicks(table: any[], field: string | undefined): number[] | undefined { + if (!field || !Array.isArray(table) || table.length === 0) return undefined; + let min = Infinity; + let max = -Infinity; + for (const row of table) { + const v = Number(row?.[field]); + if (!Number.isFinite(v) || v <= 0) continue; + if (v < min) min = v; + if (v > max) max = v; + } + if (!Number.isFinite(min) || !Number.isFinite(max)) return undefined; + const lo = Math.floor(Math.log10(min)); + const hi = Math.ceil(Math.log10(max)); + if (hi - lo < 2) return undefined; + const out: number[] = []; + for (let k = lo; k <= hi; k++) out.push(10 ** k); + return out; +} + +/** + * The values a field actually holds, for an index axis that should be ticked + * at observations rather than at round numbers between them. + * + * Returns `undefined` when there is nothing to improve on: too few values to + * matter, or so many that stating them all would be a worse fence than the + * renderer's own choice. + */ +const MAX_OBSERVED_TICKS = 30; + +function observedTicks( + table: any[], + field: string | undefined, + mode: 'observed' | 'endpoints' | 'sparse', + span: number, + fontSize: number, +): any[] | undefined { + if (!field || !Array.isArray(table) || table.length === 0) return undefined; + const seen = new Map(); + for (const row of table) { + const v = row?.[field]; + if (v == null) continue; + const k = String(v); + if (!seen.has(k)) seen.set(k, v); + } + const values = [...seen.values()]; + if (values.length < 2 || values.length > MAX_OBSERVED_TICKS) return undefined; + values.sort((a, b) => { + const na = Number(a); + const nb = Number(b); + if (Number.isFinite(na) && Number.isFinite(nb)) return na - nb; + return String(a) < String(b) ? -1 : 1; + }); + if (mode === 'endpoints') return [values[0], values[values.length - 1]]; + + // Every label needs its own width. Where there is not room for all of + // them, take every k-th — and keep the last, which is where a reader + // looks for "now". + const widest = Math.max(...values.map((v) => String(v).length)); + const room = Math.max(2, Math.floor(span / (widest * fontSize * 0.6 + 10))); + if (values.length <= room) return values; + const step = Math.ceil((values.length - 1) / (room - 1)); + if (step <= 1) return values; + const out: any[] = []; + for (let i = 0; i < values.length; i += step) out.push(values[i]); + if (out[out.length - 1] !== values[values.length - 1]) out.push(values[values.length - 1]); + return out; +} + +const DAY_MS = 86400000; +const MONTH_MS = 30.44 * DAY_MS; +const YEAR_MS = 365.25 * DAY_MS; + +/** Signs that go before the number rather than after it. */ +const PREFIX_UNITS = new Set(['$', '£', '€', '¥', '₹', '₩', 'R$', 'US$', 'A$', 'CA$', 'CHF']); + +/** + * A label expression that writes the unit onto the ticks the house asked for. + * + * Builds on whatever expression is already there — flint's own number format + * may have written one — because that expression *is* the label, and the unit + * goes outside it. + */ +function tagWithUnit( + existing: string | undefined, + unit: string, + where: 'firstTick' | 'lastTick' | 'firstAndLast' | 'everyTick', +): string { + const label = existing ? `(${existing})` : 'datum.label'; + // `10%` but `420 ppm` — a symbol sits against the number, a word does not. + const quoted = JSON.stringify(/^[A-Za-z]/.test(unit) ? ` ${unit}` : unit); + const tagged = PREFIX_UNITS.has(unit) + ? `${quoted} + ${label}` + : `${label} + ${quoted}`; + if (where === 'everyTick') return tagged; + const at = where === 'firstTick' ? 'datum.index === 0' + : where === 'lastTick' ? 'datum.index === 1' + : 'datum.index === 0 || datum.index === 1'; + return `${at} ? ${tagged} : ${label}`; +} + +/** The field a unit-bearing label reads from. */ +const UNIT_LABEL_FIELD = '__flintValueWithUnit'; + +/** + * Print a value with its unit — `65%`, `$4.2bn`. + * + * Vega-Lite can format a number or write a literal, never both, and it takes no + * expression on a `text` channel. So the label is computed once for the whole + * plot body and the text layer simply reads the new field. + */ +function printWithUnit(body: any, node: any, field: string, format: string | undefined, unit: string): void { + const value = `datum[${JSON.stringify(field)}]`; + const shown = format ? `format(${value}, ${JSON.stringify(format)})` : `${value} + ''`; + const quoted = JSON.stringify(/^[A-Za-z]/.test(unit) ? ` ${unit}` : unit); + const calculate = PREFIX_UNITS.has(unit) ? `${quoted} + ${shown}` : `${shown} + ${quoted}`; + const transform = (body.transform ??= []); + if (!transform.some((t: any) => t.as === UNIT_LABEL_FIELD)) transform.push({ calculate, as: UNIT_LABEL_FIELD }); + node.encoding.text = { field: UNIT_LABEL_FIELD, type: 'nominal' }; +} + +/** + * The unit on values the *template* printed. + * + * Where a template already writes a number beside every mark, the theme leaves + * the label alone — but the house's unit still has nowhere else to go, and a + * bare `65` beside a slice is not the same statement as `65%`. + */ +function applyPrintedUnits(spec: any, d: DesignDecisions, say: (p: string, m: string) => void): void { + const unit = d.dataLabels.unit; + if (!unit) return; + let said = false; + for (const body of plotBodies(spec)) { + for (const node of (body.layer ?? [body])) { + if (markTypeOf(node.mark) !== 'text') continue; + const text = node.encoding?.text; + // Only numbers take a unit: a name printed at the end of a line is + // already a word, and a label already reading the computed field + // says what it means. + if (!text?.field || text.type !== 'quantitative') continue; + printWithUnit(body, node, text.field, text.format ?? d.dataLabels.format, unit); + if (!said) { + say('annotation.unit', + `each printed value carries its unit \`${unit}\` — there is no axis left to state it on`); + said = true; + } + } + } +} + +/** `2012`, `2012-07`, `2012-07-27` — a date with no time and no zone in it. */ +const CALENDAR_DATE = /^(\d{4})(?:-(\d{2})(?:-(\d{2}))?)?$/; + +/** + * The dates the data actually holds, as Vega-Lite `DateTime` objects. + * + * Only for fields written as plain calendar dates: those are read as UTC, so + * the calendar fields can be handed back untouched and cannot drift by an + * offset — provided the scale is told to keep UTC too, which is why the caller + * also sets `scale.type`. A format comes with them, because an axis given + * explicit values loses the one the renderer would have inferred and falls + * back to clock time. + */ +function observedDates( + table: any[], + field: string | undefined, + mode: 'observed' | 'endpoints' | 'sparse', + span: number, + fontSize: number, +): { values: Record[]; format: string } | undefined { + if (!field || !Array.isArray(table) || table.length === 0) return undefined; + const seen = new Set(); + for (const row of table) { + const v = row?.[field]; + if (typeof v !== 'string' || !CALENDAR_DATE.test(v)) return undefined; + seen.add(v); + } + const dates = [...seen].sort(); + if (dates.length < 2 || dates.length > MAX_OBSERVED_TICKS) return undefined; + + const parts = dates.map((s) => s.match(CALENDAR_DATE)!); + const grain = parts.every((p) => p[2] == null) ? 'year' + : parts.every((p) => p[3] == null) ? 'month' : 'day'; + const format = grain === 'year' ? '%Y' : grain === 'month' ? '%b %Y' : '%b %-d'; + const width = (grain === 'year' ? 4 : grain === 'month' ? 8 : 6) * fontSize * 0.6 + 10; + + let kept = parts; + if (mode === 'endpoints') { + kept = [parts[0], parts[parts.length - 1]]; + } else { + const room = Math.max(2, Math.floor(span / width)); + if (parts.length > room) { + const step = Math.ceil((parts.length - 1) / (room - 1)); + kept = parts.filter((_, i) => i % step === 0); + if (kept[kept.length - 1] !== parts[parts.length - 1]) kept.push(parts[parts.length - 1]); + } + } + const values = kept.map((p) => { + const dt: Record = { year: Number(p[1]), utc: true }; + if (grain !== 'year') dt.month = Number(p[2]); + if (grain === 'day') dt.date = Number(p[3]); + return dt; + }); + return { values, format }; +} + +/** + * How a temporal index axis is spaced, as an interval and a step — Olympic + * years are every 4 years, a monthly series is every month. Thinned to what + * the span can label. `undefined` when the dates are not evenly spaced enough + * for a step to be honest. + */ +function temporalStep( + table: any[], + field: string | undefined, + span: number, + fontSize: number, +): { interval: 'year' | 'month' | 'day'; step: number } | undefined { + if (!field || !Array.isArray(table) || table.length === 0) return undefined; + const stamps = new Set(); + for (const row of table) { + const v = row?.[field]; + if (v == null) continue; + const t = v instanceof Date ? v.getTime() : new Date(v as any).getTime(); + if (Number.isFinite(t)) stamps.add(t); + } + const sorted = [...stamps].sort((a, b) => a - b); + if (sorted.length < 3 || sorted.length > MAX_OBSERVED_TICKS) return undefined; + + let gap = Infinity; + for (let i = 1; i < sorted.length; i++) gap = Math.min(gap, sorted[i] - sorted[i - 1]); + let interval: 'year' | 'month' | 'day'; + let unit: number; + if (gap >= YEAR_MS * 0.9) { interval = 'year'; unit = YEAR_MS; } + else if (gap >= MONTH_MS * 0.9) { interval = 'month'; unit = MONTH_MS; } + else if (gap >= DAY_MS * 0.9) { interval = 'day'; unit = DAY_MS; } + else return undefined; + let step = Math.max(1, Math.round(gap / unit)); + + // Labels need their own width; where there is not room for one per + // observation the step multiplies. + const width = (interval === 'year' ? 4 : 8) * fontSize * 0.6 + 10; + const room = Math.max(2, Math.floor(span / width)); + const count = Math.round((sorted[sorted.length - 1] - sorted[0]) / (unit * step)) + 1; + if (count > room) step *= Math.ceil(count / room); + return { interval, step }; +} + +// --------------------------------------------------------------------------- +// Marks +// --------------------------------------------------------------------------- + +/** + * A dash that carries a *distinction* — observed against projected — has to + * survive the house's line style. A rounded cap extends every segment by half + * the stroke width at each end, so at the widths a display theme likes, the + * default dash pattern closes up into a solid line. Where `strokeDash` is + * encoded, the cap goes square and the pattern is scaled to the stroke. + */ +function protectDashEncoding(spec: any, config: any, strokeWidth: number): void { + let dashed = false; + walk(spec, (node) => { + if (node.encoding?.strokeDash?.field) dashed = true; + }); + if (!dashed) return; + config.line = { ...config.line, strokeCap: 'butt' }; + const w = Math.max(1, strokeWidth); + const range = [[1, 0], [w * 3, w * 2], [w * 1.2, w * 1.2], [w * 5, w * 2, w * 1.2, w * 2]]; + walk(spec, (node) => { + const enc = node.encoding?.strokeDash; + if (!enc?.field || enc.scale?.range) return; + enc.scale = { ...(enc.scale ?? {}), range }; + }); +} + +function applyMarks(spec: any, d: DesignDecisions, table: any[], say: (p: string, m: string) => void): void { + const config = spec.config; + const m = d.marks; + + config.line = { ...(config.line ?? {}), strokeWidth: m.strokeWidth }; + config.trail = { ...(config.trail ?? {}), size: m.strokeWidth }; + if (m.strokeCap) { config.line.strokeCap = m.strokeCap; config.rule = { ...(config.rule ?? {}), strokeCap: m.strokeCap }; } + if (m.strokeJoin) config.line.strokeJoin = m.strokeJoin; + if (m.interpolate) config.line.interpolate = m.interpolate; + if (m.fillOpacity != null) config.area = { ...(config.area ?? {}), fillOpacity: m.fillOpacity }; + protectDashEncoding(spec, config, m.strokeWidth); + + if (m.point?.show) { + config.line.point = { + filled: m.point.filled !== false, + size: m.point.size ?? 24, + ...(m.point.haloColor ? { stroke: m.point.haloColor, strokeWidth: m.point.haloWidth ?? 1 } : {}), + }; // A point on a line marks a reading. A fitted line has no readings — + // it is sampled wherever the fit needs sampling, which for a straight + // one is the two ends — so a point at each vertex claims two + // observations that were never made, sitting exactly on the model. + let saidFit = false; + walk(spec, (node) => { + if (!LINE_MARKS.has(markTypeOf(node.mark) ?? '')) return; + if (!(node.transform ?? []).some((t: any) => t?.regression || t?.loess)) return; + node.mark = { ...normalizeMark(node.mark), point: false }; + if (!saidFit) { + say('marks.point', 'the fitted line keeps no vertex points — its vertices are where the fit was sampled, not where anything was measured'); + saidFit = true; + } + }); + + // Dots are worth drawing while they are still countable. A house that + // marks its readings means the handful a reader could point at one by + // one; past that the dots touch, fuse into a beaded rope, and hide the + // shape of the very line they sit on — a step curve with one vertex + // per observation is thirty dots along a curve nobody can now follow. + // A spec that asked for points itself keeps them; this only decides + // whether the *house* adds them where the chart never asked. + let saidCrowd = false; + walk(spec, (node) => { + if (!LINE_MARKS.has(markTypeOf(node.mark) ?? '')) return; + const mark = normalizeMark(node.mark); + if (mark.point !== undefined) return; + const enc = mergedEncoding(node, spec.encoding); + const readings = maxReadingsPerSeries(enc, table); + if (readings <= MAX_DOTTED_READINGS) return; + node.mark = { ...mark, point: false }; + if (!saidCrowd) { + say('marks.point', + `${readings} readings on one line is past the ${MAX_DOTTED_READINGS} a reader can take one at a time, so the house's dots stand down and the line keeps its shape`); + saidCrowd = true; + } + }); + } + + // The ring of page around a dot is settled in `config.line.point` for the + // lines the house dotted itself. A chart that asked for its own vertices + // — a bump, a slope — carries `point: true` on the mark, and a bare + // `true` takes the renderer's dot and none of the house's, so the ring + // has to be written onto those marks by hand. + if (m.point?.haloColor) { + const dot = m.point; + walk(spec, (node) => { + if (!LINE_MARKS.has(markTypeOf(node.mark) ?? '')) return; + const mark = normalizeMark(node.mark); + if (!mark.point) return; + mark.point = { + ...(typeof mark.point === 'object' ? mark.point : {}), + ...(dot.filled != null ? { filled: dot.filled } : {}), + ...(dot.size != null ? { size: dot.size } : {}), + stroke: dot.haloColor, + strokeWidth: dot.haloWidth ?? 1, + }; + node.mark = mark; + }); + } + // The same size wherever a dot is drawn — and `config.point` alone does + // not reach them all: `circle` and `square` are their own mark types with + // their own config blocks, and most scatters are drawn as one of those, so + // a house that sized only `point` silently missed every scatter it had. + if (m.point?.size != null || m.point?.filled != null) { + for (const family of ['point', 'circle', 'square'] as const) { + config[family] = { + ...(config[family] ?? {}), + ...(m.point.size != null ? { size: m.point.size } : {}), + filled: m.point.filled !== false, + }; + } + if (m.point.size != null) { + say('marks.point.size', + `a dot is drawn at ${m.point.size}px² wherever one is drawn — the house's size, not the renderer's, and the layout may still shrink it`); + } + } + + // Band occupancy is a scale decision, not a mark size: expressing it as + // padding keeps grouped and simple bars consistent and leaves the layout + // engine's step untouched. + // + // The positional encoding a mark reads is not always its own — a layered + // template states the category once on the parent and lets every layer + // inherit it. The scale lives wherever the encoding was written, so the + // walk has to carry what it inherited or a house's bar width silently + // misses every layered chart. + const paddingInner = clamp(1 - m.bandFraction, 0, 0.9); + let saidCells = false; + let saidBand = false; + const bandWalk = (node: any, inherited: any): void => { + if (!node || typeof node !== 'object') return; + const enc = mergedEncoding(node, inherited); + const mark = markTypeOf(node.mark); + if (mark === 'bar' || mark === 'rect' || mark === 'boxplot') { + const discrete = (['x', 'y'] as const).filter((channel) => { + const t = enc[channel]?.type; + return enc[channel]?.field && (t === 'nominal' || t === 'ordinal'); + }); + // A rect with a category on *both* axes is a cell in a grid, not a + // bar in a row: the cell is the band, and a house's bar thickness + // says nothing about it. Thinning it here opens gaps in what should + // read as a continuous surface, and how big the cell is was settled + // by the layout, which is the only pass that knows the room. + if (mark === 'rect' && discrete.length === 2) { + if (!saidCells) { + say('marks.bandFraction', + 'the marks are cells in a grid, not bars in a row — band occupancy is a bar rule and does not apply'); + saidCells = true; + } + } else { + for (const channel of discrete) { + const target = node.encoding?.[channel] ?? inherited?.[channel]; + if (!target) continue; + target.scale = { ...(target.scale ?? {}), paddingInner }; + // A template that pinned its bars to a pixel width fixed the + // gap along with it. The house sets the gap, so the width + // has to move with it. + if (typeof (node.mark as any)?.size === 'number' && mark !== 'boxplot') { + const step = bandStep(spec, node, enc, channel, table); + if (step) { + node.mark = { ...normalizeMark(node.mark), size: Math.max(1, Math.round(step * m.bandFraction)) }; + if (!saidBand) { + say('marks.bandFraction', + `the template pinned its bars to a pixel width — they are re-cut to ${Math.round(m.bandFraction * 100)}% of the band`); + saidBand = true; + } + } + } + } + } + } + // A box is a summary, and how much of its band it fills is a house + // matter of its own: a wide box reads as a distribution, a narrow one + // as a marker with error bars. + if (mark === 'boxplot' && m.summary?.widthFraction != null) { + const channel = (['x', 'y'] as const) + .find((c) => enc[c]?.field && (enc[c]?.type === 'nominal' || enc[c]?.type === 'ordinal')); + const lanes = laneCount(node, enc, table); + const step = channel ? bandStep(spec, node, enc, channel, table) : undefined; + if (step) { + const size = Math.max(3, Math.round((step * m.summary.widthFraction) / lanes)); + node.mark = { ...normalizeMark(node.mark), size }; + say('marks.summary.widthFraction', + `the house fills ${Math.round(m.summary.widthFraction * 100)}% of the band with the box — ${size}px of a ${Math.round(step)}px band`); + } + } + for (const key of ['layer', 'vconcat', 'hconcat', 'concat']) { + if (Array.isArray(node[key])) node[key].forEach((c: any) => bandWalk(c, enc)); + } + if (node.spec) bandWalk(node.spec, enc); + if (node.facet?.spec) bandWalk(node.facet.spec, enc); + }; + bandWalk(spec, undefined); + + // A sized mark reads by area, and how much area the largest circle may take + // is a house matter — a page of small multiples cannot spend what a full + // page can. + if (m.sizeRange || m.minSize != null) { + let saidSize = false; + walk(spec, (node) => { + const enc = node.encoding?.size; + if (!enc?.field || enc.type !== 'quantitative') return; + enc.scale = { + ...(enc.scale ?? {}), + ...(m.sizeRange ? { range: m.sizeRange } : {}), + ...(m.minSize != null && !m.sizeRange ? { rangeMin: m.minSize } : {}), + }; + if (!saidSize && m.sizeRange) { + say('marks.sizeRange', + `sized marks run from ${m.sizeRange[0]} to ${m.sizeRange[1]}px² — the house's range, not the renderer's`); + saidSize = true; + } + }); + } + + if (m.separator?.show) { + walk(spec, (node) => { + const mark = markTypeOf(node.mark); + if (mark !== 'bar' && mark !== 'rect') return; + if (isLiteralMark(node)) return; + // A cell in a grid is not a bar in a row: it adjoins on both axes + // and its fill is the reading, so how it is held apart is its own + // decision, taken below. + if (isGridCell(node, node.encoding ?? {})) return; + node.mark = { ...normalizeMark(node.mark), stroke: m.separator!.color, strokeWidth: m.separator!.width }; + }); + } + + if (m.tile) applyTileGap(spec, m.tile, say); + + if (m.slice) applySliceGap(spec, m.slice, table, say); +} + +/** + * A cell of a grid — a heatmap, a calendar, a matrix. Both of its axes are + * spent on position, so unlike a bar it has no free axis to be thinned along: + * the gap has to be cut out of the shape. Binned axes count as discrete here, + * because a binned rect is a cell whose category happens to be a range. + */ +function isGridCell(node: any, enc: any): boolean { + if (markTypeOf(node.mark) !== 'rect') return false; + const gridded = (['x', 'y'] as const).filter((channel) => { + const e = enc[channel]; + if (!e?.field) return false; + return e.type === 'nominal' || e.type === 'ordinal' || e.bin != null; + }); + return gridded.length === 2; +} + +/** + * Cells are cut apart rather than spaced apart, for the same reason wedges + * are: spacing them would mean shrinking the mark, and on a grid the mark's + * extent is what says which month and which city the reading belongs to. + * + * A stroke straddles the edge it is drawn on, so a shared edge painted at + * `gap` px opens a `gap`-wide channel between the two cells — half taken from + * each, which keeps the grid's centres where the scale put them. + */ +function applyTileGap( + spec: any, + tile: NonNullable, + say: (p: string, m: string) => void, +): void { + let said = false; + const gridWalk = (node: any, inherited: any): void => { + if (!node || typeof node !== 'object') return; + const enc = mergedEncoding(node, inherited); + if (!isLiteralMark(node) && isGridCell(node, enc)) { + node.mark = { ...normalizeMark(node.mark), stroke: tile.color, strokeWidth: tile.gap }; + if (!said) { + say('marks.tile', + `the cells are cut apart by ${tile.gap}px — the grid reads as a table of separate readings rather than one continuous field`); + said = true; + } + } + for (const key of ['layer', 'vconcat', 'hconcat', 'concat']) { + if (Array.isArray(node[key])) node[key].forEach((child: any) => gridWalk(child, enc)); + } + if (node.spec) gridWalk(node.spec, enc); + if (node.facet?.spec) gridWalk(node.facet.spec, enc); + }; + gridWalk(spec, undefined); +} + +/** + * A connector draws the *distance* between two positions of one datum, and + * both positions are already on the page. It is structure, not a series — so + * it takes the house's structural ink, and it runs after the series ink has + * been laid down so that the ink it takes is the one that stands. + * + * What it is worth, though, depends on what it joins, and that is a question + * the spec answers without anyone naming a chart type: + * + * - a **stem** ends at a constant — the baseline. The dot at its other end + * already carries the value, so the stem is only a path for the eye and + * is drawn at a hairline. + * - a **bridge** ends at another mark across the *measured* axis. Nothing + * else on the plot states the gap between men and women, before and + * after: the bridge *is* that reading, and a hairline asks the eye to + * measure something it can hardly see. It is drawn at a mark's weight. + * - a **lead** ends at another mark across the *categorical* axis, holding + * one value the whole way — a waterfall's step from one bar's top to + * where the next one starts. It carries no distance of its own; the two + * bar tops it touches already say where the level is. At a mark's weight + * it would be the heaviest line on a chart that is asking about the bars, + * so it is drawn at a hairline like the stem. + */ +function applyConnectors(spec: any, d: DesignDecisions, say: (p: string, m: string) => void): void { + const c = d.marks.connector; + if (!c?.show) return; + + const said = new Set(); + const paint = (node: any, role: 'stem' | 'bridge' | 'lead'): void => { + const mark = normalizeMark(node.mark); + if (c.color) mark.color = c.color; + mark.strokeWidth = role === 'bridge' ? c.spanWidth : c.width; + if (c.dash) mark.strokeDash = c.dash; + node.mark = mark; + if (said.has(role)) return; + said.add(role); + const why = { + bridge: `the bridge is drawn at ${c.spanWidth}px in structural ink — the distance it spans is the reading, so it carries a mark's weight and none of a series' colour`, + stem: `the stem is drawn at ${c.width}px in structural ink — it leads the eye to the axis and states nothing the dot's position has not`, + lead: `the lead line is drawn at ${c.width}px in structural ink — it runs across the categories at one level, and the two mark ends it touches already state that level`, + }[role]; + say('marks.connector', why); + }; + + const bandWalk = (node: any, inherited: any): void => { + if (!node || typeof node !== 'object') return; + const enc = mergedEncoding(node, inherited); + const type = markTypeOf(node.mark); + const own = node.encoding ?? {}; + if (!isLiteralMark(node)) { + if (type === 'rule') { + // One end is written on the mark; the other is where it stops. + // If it stops at a number, it stops at the baseline. If it + // stops at another mark, which axis it crossed to get there + // decides whether the crossing was the reading. + const along = (own.x2 ?? enc.x2) ? 'x' : (own.y2 ?? enc.y2) ? 'y' : undefined; + const end = along === 'x' ? (own.x2 ?? enc.x2) : along === 'y' ? (own.y2 ?? enc.y2) : undefined; + if (end) { + const start = along === 'x' ? (own.x ?? enc.x) : (own.y ?? enc.y); + const acrossCategories = start?.type === 'nominal' || start?.type === 'ordinal'; + paint(node, !end.field ? 'stem' : acrossCategories ? 'lead' : 'bridge'); + } + } else if (type === 'line') { + // A line grouped by the field the categorical axis already + // names draws one segment inside each band, not one series + // across them: it joins that band's own marks. + const key = own.detail?.field ?? enc.detail?.field; + const band = (['x', 'y'] as const) + .map((ch) => enc[ch]) + .find((e: any) => e?.field && (e.type === 'nominal' || e.type === 'ordinal')); + if (key && band?.field === key) paint(node, 'bridge'); + } + } + for (const k of ['layer', 'vconcat', 'hconcat', 'concat']) { + if (Array.isArray(node[k])) node[k].forEach((child: any) => bandWalk(child, enc)); + } + if (node.spec) bandWalk(node.spec, enc); + if (node.facet?.spec) bandWalk(node.facet.spec, enc); + }; + bandWalk(spec, undefined); +} + +/** + * How far a mark reaches from the point it is anchored at. Only the round + * marks have one; a bar, an area or a line is drawn *to* its anchor, so the + * anchor is already the edge. + * + * Vega-Lite states a point's `size` as an area in px², which is how the size + * channel is read as well — so the radius is the same conversion in both. + */ +const POINT_MARKS = new Set(['point', 'circle', 'square']); +function markRadius(node: any, spec: any, d: DesignDecisions): number { + const type = markTypeOf(node.mark) ?? ''; + if (!POINT_MARKS.has(type)) return 0; + const area = normalizeMark(node.mark)?.size + ?? node.encoding?.size?.scale?.range?.[1] + ?? spec.config?.[type]?.size + ?? spec.config?.point?.size + ?? d.marks.point?.size + ?? 30; + return typeof area === 'number' ? Math.round(Math.sqrt(area / Math.PI)) : 0; +} + +/** + * Wedges are cut apart rather than spaced apart: a pie has no band to give + * back, so the gap has to come out of the shape itself — either painted over + * the shared edge or opened by swinging the two arcs away from each other. + */ +function applySliceGap( + spec: any, + slice: NonNullable, + table: any[], + say: (p: string, m: string) => void, +): void { + let said = false; + walk(spec, (node) => { + if (markTypeOf(node.mark) !== 'arc') return; + if (isLiteralMark(node)) return; + const mark = normalizeMark(node.mark); + + if (slice.style === 'pad') { + const radius = arcRadius(node, spec, mark); + if (!radius) return; + // A gap is a gap only while there is a wedge left on either side of + // it. Five slices at 2px cost the circle almost nothing; forty cost + // it the pie. The house's width is honoured until the gaps would + // take a sixth of the circumference between them, and held there. + const enc = node.encoding ?? {}; + const slices = distinctCount(table, enc.color?.field ?? enc.theta?.field); + const wanted = slice.gap / radius; + const room = slices ? (2 * Math.PI * 0.15) / slices : wanted; + mark.padAngle = Math.min(wanted, room); + if (!said) { + say('marks.slice.gap', wanted > room + ? `${slices} wedges cannot each give up ${slice.gap}px and still be wedges — the gap is held at ${(room * radius).toFixed(1)}px` + : `the wedges stand ${slice.gap}px apart at the rim`); + said = true; + } + } else { + mark.stroke = slice.color; + mark.strokeWidth = slice.gap; + if (!said) { + say('marks.slice.gap', + `a ${slice.gap}px rule cuts the wedges apart — two arcs of the same size read as two shapes, not one`); + said = true; + } + } + node.mark = mark; + }); +} + +/** + * How far the wedge reaches. Stated on the mark where a template pinned it; + * otherwise Vega-Lite fills the smaller side of the plot. + */ +function arcRadius(node: any, spec: any, mark: any): number | undefined { + if (typeof mark.outerRadius === 'number') return mark.outerRadius; + const w = typeof node.width === 'number' ? node.width : spec.width; + const h = typeof node.height === 'number' ? node.height : spec.height; + if (typeof w !== 'number' || typeof h !== 'number') return undefined; + return Math.min(w, h) / 2; +} + +/** + * A mark placed entirely in pixels — every channel a literal value, no field + * and no datum — is not a series. It is furniture the template drew itself: a + * card, a track, a badge. It carries no value, so a separator between "its + * segments" is a line across nothing, and the house's series ink says nothing + * about it either. + */ +function isLiteralMark(node: any): boolean { + const enc = node?.encoding; + if (!enc || typeof enc !== 'object') return false; + const channels = Object.values(enc) as any[]; + if (channels.length === 0) return false; + return channels.every((e) => e && typeof e === 'object' && e.field == null && e.datum === undefined); +} + +function normalizeMark(mark: any): any { + return typeof mark === 'string' ? { type: mark } : { ...mark }; +} + +/** Distinct values a field takes in the data behind the chart. */ +function distinctCount(table: any[], field: string | undefined): number { + if (!field || !Array.isArray(table)) return 0; + const seen = new Set(); + for (const row of table) { + const v = row?.[field]; + if (v != null) seen.add(String(v)); + } + return seen.size; +} + +/** How many dots one line can carry before they stop being countable. */ +const MAX_DOTTED_READINGS = 12; + +/** + * The longest single line in the plot, counted in readings. + * + * A reading is one position along the line, so it is the distinct values of + * the field on the continuous axis, taken within one series — the series + * being whatever splits the line into more than one: colour, detail, a dash. + * Rows would be the wrong count, since an aggregated line draws one vertex + * from many rows. + */ +function maxReadingsPerSeries(enc: any, table: any[]): number { + if (!Array.isArray(table) || table.length === 0) return 0; + + const along = (['x', 'y'] as const) + .map((ch) => enc?.[ch]) + .find((e: any) => e?.field && e.type !== 'nominal' && e.type !== 'ordinal') + ?? enc?.x; + if (!along?.field) return 0; + + const keys = ['color', 'detail', 'strokeDash', 'shape'] + .map((ch) => enc?.[ch]?.field) + .filter((f: any): f is string => typeof f === 'string' && f !== along.field); + + const perSeries = new Map>(); + for (const row of table) { + const v = row?.[along.field]; + if (v == null) continue; + const key = keys.map((f) => String(row?.[f])).join('\u0000'); + let seen = perSeries.get(key); + if (!seen) perSeries.set(key, seen = new Set()); + seen.add(String(v)); + } + + let max = 0; + for (const seen of perSeries.values()) max = Math.max(max, seen.size); + return max; +} + +/** + * How many pixels one category gets. The renderer works this out for itself, + * but a pass that wants to re-cut a mark pinned in pixels has to arrive at the + * same number independently. + * + * A band scale usually states it outright — `width: {step: 46}` *is* the step, + * and dividing a plot width by the categories would answer a question nobody + * asked. Where neither the step nor a pixel size is stated the answer is that + * it is not known: a guess here re-cuts every mark on the chart against a + * number that came from nowhere. + */ +function bandStep(spec: any, node: any, enc: any, channel: 'x' | 'y', table: any[]): number | undefined { + const size = channel === 'x' ? (node.width ?? spec.width) : (node.height ?? spec.height); + if (size && typeof size === 'object' && typeof size.step === 'number') return size.step; + if (typeof size !== 'number') return undefined; + const count = distinctCount(table, enc?.[channel]?.field); + if (!count) return undefined; + return size / count; +} + +/** Side-by-side lanes inside one band, from the offset channel if there is one. */ +function laneCount(node: any, enc: any, table: any[]): number { + const offset = enc?.xOffset ?? enc?.yOffset; + if (!offset?.field) return 1; + if (offset.type === 'quantitative') { + return Math.max(1, distinctCount(table, enc?.color?.field) || 1); + } + return Math.max(1, distinctCount(table, offset.field) || 1); +} + +// --------------------------------------------------------------------------- +// Series ink +// --------------------------------------------------------------------------- + +/** + * Properties that describe *another* colour decision. Once the theme supplies + * an explicit range they are not merely redundant, they conflict — a `scheme` + * silently wins over `range`, and a `domainMid` on a quantize scale is a + * runtime error. + */ +function clearColorScale(scale: any): any { + const out = { ...scale }; + delete out.scheme; + delete out.interpolate; + delete out.domainMid; + delete out.reverse; + return out; +} + +/** A colour channel whose scale is continuous, and so cannot take a one-stop range. */ +function isContinuousColor(enc: any): boolean { + return enc?.type === 'quantitative' || enc?.type === 'temporal'; +} + +/** + * Write an explicit colour range, clearing whatever colour decision was there + * before. A continuous scale interpolates between range entries, so a single + * ink has to be stated twice — one stop is a runtime error in Vega, not a + * constant colour. + */ +function setColorRange(enc: any, range: string[], extra?: any): void { + const r = isContinuousColor(enc) && range.length < 2 ? [range[0], range[0]] : range; + enc.scale = { ...clearColorScale(enc.scale ?? {}), ...(extra ?? {}), range: r }; +} + +/** + * A near-neutral ink baked into a template was chosen against flint's own + * surface, which is white. What it means is its *distance* from that surface — + * `#f5f5f5` is a faint tint, `#1a1a1a` is a hard marker — and carried onto a + * dark surface both readings invert. Re-place each at the same distance from + * the surface it now sits on. Anything with real hue is left alone: that was a + * choice about meaning, not about contrast. + */ +function reToneNeutral(ink: string, surface: string): string { + const c = parseColor(ink); + const bg = parseColor(surface); + if (!c || !bg) return ink; + if (Math.max(c.r, c.g, c.b) - Math.min(c.r, c.g, c.b) > 24) return ink; + const vb = (bg.r + bg.g + bg.b) / 3; + if (vb > 128) return ink; + const v = (c.r + c.g + c.b) / 3; + const t = Math.round(Math.min(255, vb + (255 - v))); + return toHex({ r: t, g: t, b: t, a: 1 }); +} + +const NEGATIVE_WORDS = /\b(below|decrease|decline|loss|losses|negative|down|fall|deficit|shrink|worse)\b/i; +const POSITIVE_WORDS = /\b(above|increase|growth|gain|gains|positive|up|rise|surplus|grow|better)\b/i; + +/** + * The status inks name roles — positive, negative, neutral — and a role is a + * property of the *category*, not of its place in the domain. Handing the + * three colours to the scale in a fixed order paints whichever series happens + * to sort first as the negative one, which is how "above average" ends up red. + * So pair each domain value with a role: by what it calls itself where the + * label says so, and otherwise by the sign of the quantity it carries. + */ +function statusRange( + enc: any, + node: any, + status: { positive?: string; negative?: string; neutral?: string }, + table: any[], +): { domain: any[]; range: string[]; roles: string[] } | undefined { + if (isContinuousColor(enc)) return undefined; + const field = enc.field; + const domain: any[] = Array.isArray(enc.scale?.domain) && enc.scale.domain.length + ? enc.scale.domain + : [...new Set((table ?? []).map((r) => r?.[field]).filter((v) => v != null))]; + if (domain.length < 2) return undefined; + + // The measure the categories are signing: the quantitative position on the + // same node, y before x, since a signed bar is usually vertical. + let measure: string | undefined; + for (const ch of ['y', 'x', 'theta', 'size'] as const) { + const e = node?.encoding?.[ch]; + if (e?.field && (e.type === 'quantitative' || e.type == null)) { measure = e.field; break; } + } + + const roles = domain.map((value) => { + const label = String(value); + if (NEGATIVE_WORDS.test(label)) return 'negative'; + if (POSITIVE_WORDS.test(label)) return 'positive'; + if (!measure) return 'neutral'; + let sum = 0; let n = 0; + for (const row of table ?? []) { + if (row?.[field] !== value) continue; + const v = Number(row?.[measure]); + if (Number.isFinite(v)) { sum += v; n++; } + } + if (n === 0) return 'neutral'; + return sum < 0 ? 'negative' : sum > 0 ? 'positive' : 'neutral'; + }); + + // No sign to speak of — every category reads the same way, so the status + // set says nothing the categorical set would not say better. + if (new Set(roles).size < 2) return undefined; + const range = roles.map((role) => (status as any)[role] ?? status.neutral ?? status.positive); + if (range.some((c) => !c)) return undefined; + return { domain, range: range as string[], roles }; +} + +function applySeriesInk(spec: any, d: DesignDecisions, table: any[], say: (p: string, m: string) => void): void { + const s = d.series; + let sawColorField = false; + let saidCollapse = false; + let saidStatus = false; + let saidExhausted = false; + + /** The house's indexed set, extended to `n` positions with the overflow ink. */ + const palette = (n: number): string[] => { + const out = s.categorical.slice(0, n); + while (out.length < n) { + out.push(s.overflow ?? s.categorical[out.length % Math.max(1, s.categorical.length)] ?? s.single); + } + return out.length ? out : [s.single]; + }; + + walk(spec, (node) => { + for (const channel of ['color', 'fill', 'stroke'] as const) { + const enc = node.encoding?.[channel]; + if (!enc || !enc.field) continue; + sawColorField = true; + + // A scale that already names its domain has told us how many inks + // it needs. Writing fewer would not restyle the chart, it would + // erase a distinction the chart was built to make. + const declared = Array.isArray(enc.scale?.domain) ? enc.scale.domain.length : 0; + + if (s.mode === 'single') { + if (declared > 1 && !isContinuousColor(enc)) { + if (!saidCollapse) { + say('ink.series', + `grounding saw one series but \`${enc.field}\` declares ${declared} colour values — the house's categorical set is used rather than flattening them`); + saidCollapse = true; + } + setColorRange(enc, palette(declared)); + } else { + setColorRange(enc, [s.single]); + } + continue; + } + if (s.mode === 'status' && s.status) { + const signed = statusRange(enc, node, s.status, table); + if (signed) { + setColorRange(enc, signed.range, { domain: signed.domain }); + if (!saidStatus) { + say('ink.series.status', + `the categories carry a sign — ${signed.domain.map((v, i) => `${v} is ${signed.roles[i]}`).join(', ')}`); + saidStatus = true; + } + } else { + setColorRange(enc, [s.status.negative, s.status.neutral, s.status.positive].filter(Boolean) as string[]); + } + continue; + } + if ((s.mode === 'sequential' || s.mode === 'diverging') && s.range?.length) { + const quantize = s.quantize && enc.type === 'quantitative'; + setColorRange(enc, s.range, quantize ? { type: 'quantize' } : undefined); + continue; + } + // Categorical: an ordered set consumed by index, with the overflow + // ink taking every position past the end. A count of zero means + // grounding could not count the series, so the whole set is + // offered and the scale takes what it needs. + const need = Math.max(d.bound.seriesCount || s.categorical.length || 1, declared); + if (s.exhausted && need > s.categorical.length) { + // Grounding found the house short of inks and short of an + // answer for the remainder. Whatever scheme is on the chart + // was picked for this many categories; a shorter set repainted + // over it would only make two things look like one. + if (!saidExhausted) { + say('ink.series.categorical', + `${need} series against ${s.categorical.length} house inks — the scale is left as it is so no two series share a colour`); + saidExhausted = true; + } + continue; + } + setColorRange(enc, palette(need)); + } + }); + + // No colour channel at all: every data mark takes the single-series ink. + // Unless it already states one. A layer that hard-codes its own colour + // beside a colour-encoded layer is *context* — a bullet chart's qualitative + // bands, a target tick, a reference band. Those are roles, not series, and + // painting them the series ink erases the distinction they exist to make. + // They do still have to be re-toned: a neutral chosen against white is + // invisible on black. + let saidRole = false; + let saidChrome = false; + let saidHollow = false; + const surface = d.surface.plot ?? d.surface.canvas; + + // Furniture the template drew in pixels — a card, a track, a caption — is + // painted with literal `fill`s chosen against flint's white. Those are + // roles, not series: they keep their hues and only move to the same + // distance from the surface they now sit on. Left alone, a white card + // stays a white island on a dark canvas. + walk(spec, (node) => { + if (!markTypeOf(node.mark) || node.__themeSynthetic || !isLiteralMark(node)) return; + const mark = normalizeMark(node.mark); + let changed = false; + for (const key of ['fill', 'stroke', 'color'] as const) { + const own = mark[key]; + if (typeof own !== 'string') continue; + const toned = reToneNeutral(own, surface); + if (toned !== own) { mark[key] = toned; changed = true; } + } + if (!changed) return; + node.mark = mark; + if (!saidChrome) { + say('ink.series', + 'the template drew its own furniture in literal colours — those keep their role and are re-toned against the surface'); + saidChrome = true; + } + }); + + walk(spec, (node) => { + const mark = markTypeOf(node.mark); + if (!mark || !DATA_MARKS.has(mark)) return; + if (node.encoding?.color?.field || node.encoding?.fill?.field || node.encoding?.stroke?.field) return; + if (node.encoding?.color?.value != null || node.__themeSynthetic) return; + if (isLiteralMark(node)) return; + // A box drawn hollow is a box drawn over its own observations: the + // sample is the figure and the summary has demoted to scaffolding + // around it. Series ink on scaffolding puts the house colour on the + // part of the mark that carries no value, and leaves the box and the + // median it holds in two different colours. Both take the structural + // ink instead, and the series ink stays where the data is. + const box = normalizeMark(node.mark).box; + if (mark === 'boxplot' && box && typeof box === 'object' && box.filled === false) { + const structural = d.text.primary; + const current = normalizeMark(node.mark); + node.mark = { + ...current, + color: structural, + ...(current.median ? { median: { ...current.median, color: structural } } : {}), + }; + if (!saidHollow) { + say('ink.series', + 'the box is hollow because the observations are drawn through it — the outline is scaffolding and takes the text ink, not the series ink'); + saidHollow = true; + } + return; + } + const own = normalizeMark(node.mark).color; + if (typeof own === 'string' && sawColorField) { + const toned = reToneNeutral(own, surface); + if (toned !== own && !saidRole) { + say('ink.series', + 'the layer states its own colour beside a colour-encoded layer — it is context, not series, so it keeps its role and is only re-toned against the surface'); + saidRole = true; + } + node.mark = { ...normalizeMark(node.mark), color: toned }; + return; + } + node.mark = { ...normalizeMark(node.mark), color: s.single }; + }); + + if (!sawColorField && s.mode === 'categorical' && paintPanelSeries(spec, palette)) { + say('ink.series', + 'the series is carried by the panels of a concatenation rather than a colour channel — the house set is assigned across the panels'); + return; + } + + if (!sawColorField && s.mode !== 'single') { + say('ink.series', `grounded as \`${s.mode}\` but the chart has no colour channel — single ink used`); + } +} + +/** + * A population pyramid encodes sex by *panel*, not by a colour channel: each + * side of the concatenation hard-codes its own ink. That is still a series, + * and the house's set should land on it. Only concatenations qualify — a + * literal colour inside a layered chart is usually annotation, not series. + * + * @returns whether anything was repainted. + */ +function paintPanelSeries(spec: any, palette: (n: number) => string[]): boolean { + if (!spec.hconcat && !spec.vconcat && !spec.concat) return false; + const bodies = plotBodies(spec); + const literal = (node: any): string | undefined => { + const v = node.encoding?.color?.value ?? node.encoding?.fill?.value; + if (typeof v === 'string') return v; + const m = normalizeMark(node.mark); + return typeof m?.color === 'string' ? m.color : undefined; + }; + const targets: { node: any; ink: string }[] = []; + for (const body of bodies) { + const units = body.layer ? body.layer : [body]; + for (const unit of units) { + if (unit.__themeSynthetic) continue; + if (!DATA_MARKS.has(markTypeOf(unit.mark) ?? '')) continue; + const ink = literal(unit); + if (ink) targets.push({ node: unit, ink }); + } + } + const distinct = [...new Set(targets.map((t) => t.ink))]; + if (distinct.length < 2) return false; + const inks = palette(distinct.length); + for (const { node, ink } of targets) { + const next = inks[distinct.indexOf(ink)]; + if (node.encoding?.color?.value != null) node.encoding.color = { value: next }; + else if (node.encoding?.fill?.value != null) node.encoding.fill = { value: next }; + else node.mark = { ...normalizeMark(node.mark), color: next }; + } + return true; +} + +// --------------------------------------------------------------------------- +// Redundant channels +// --------------------------------------------------------------------------- + +/** + * Repeat the series identity on a second, non-colour channel. Vega-Lite merges + * the legends automatically as long as the field and type match, so the key + * stays one block rather than two. + */ +function applyRedundantChannels(spec: any, d: DesignDecisions, say: (p: string, m: string) => void): void { + const r = d.marks.redundant; + if (!r.shape && !r.dash) return; + + let placed = false; + walk(spec, (node) => { + const mark = markTypeOf(node.mark); + if (!mark || node.__themeSynthetic) return; + const series = node.encoding?.color ?? node.encoding?.stroke; + if (!series?.field) return; + const key = { field: series.field, type: series.type ?? 'nominal' }; + + if (r.shape && (mark === 'point' || mark === 'circle' || mark === 'square')) { + if (!node.encoding.shape) node.encoding.shape = { ...key }; + // `circle` and `square` hard-code their own shape; only `point` + // can take one from a scale. + if (mark !== 'point') node.mark = { ...normalizeMark(node.mark), type: 'point' }; + placed = true; + } + if (r.dash && LINE_MARKS.has(mark)) { + if (!node.encoding.strokeDash) node.encoding.strokeDash = { ...key }; + placed = true; + } + if (r.shape && LINE_MARKS.has(mark) && d.marks.point?.show) { + placed = true; + } + }); + + if (!placed) { + say('marks.redundantEncoding', + 'no mark in this chart can carry a redundant channel — colour is on its own'); + } +} + +// --------------------------------------------------------------------------- +// Legend +// --------------------------------------------------------------------------- + +function applyLegend(spec: any, config: any, d: DesignDecisions, table: any[], say: (p: string, m: string) => void): void { + const l = d.legend; + + if (!l.show || l.placement === 'seriesEnd' || l.placement === 'inline') { + walk(spec, (node) => { + for (const channel of ['color', 'fill', 'stroke', 'shape', 'size', 'opacity'] as const) { + const enc = node.encoding?.[channel]; + if (enc?.field) enc.legend = null; + } + }); + if (!l.show) return; + return; + } + + config.legend = { + ...(config.legend ?? {}), + orient: l.orient, + direction: l.direction, + labelFont: l.label.font, + labelFontSize: l.label.fontSize, + labelColor: l.label.color, + titleFont: l.label.font, + titleFontSize: l.label.fontSize, + titleColor: d.text.muted, + ...(l.gradientLength ? { gradientLength: l.gradientLength } : {}), + }; + // A colour ramp is a caption, not an exhibit. Left alone the renderer draws + // it at a fixed 200px, which on a small chart is most of the plot's width + // and reads as a second graphic above the first. Where the house has not + // said how long, it is cut to a share of the block it labels. + if (!l.gradientLength && (l.direction ?? 'horizontal') === 'horizontal') { + const block = blockWidth(spec, table); + if (block) { + const length = Math.round(clamp(block * 0.45, 80, 200)); + config.legend.gradientLength = length; + say('legend.gradientLength', + `the ramp runs ${length}px — under half the ${block}px block, so the key stays a caption to the chart`); + } + } + if (!l.title) config.legend.title = null; + if (l.orient === 'top-right' || l.orient === 'top-left') { + config.legend.fillColor = d.surface.plot; + config.legend.padding = 4; + config.legend.strokeColor = null; + } + + // Two keys, and only one of them is about colour. A size key inherits the + // mark's ink, which on a chart that also keys colour means its swatches + // come out the same slate as the first category's — a fifth continent, in + // effect, sitting beside the four real ones. Neutral ink says the row is + // measuring, not naming. + const namesColour = (() => { + let found = false; + walk(spec, (node) => { + const enc = node.encoding?.color ?? node.encoding?.fill; + if (enc?.field && enc.legend !== null && enc.type !== 'quantitative') found = true; + }); + return found; + })(); + if (namesColour) { + let neutralised = false; + walk(spec, (node) => { + const enc = node.encoding?.size; + if (!enc?.field || enc.legend === null) return; + enc.legend = { ...(enc.legend ?? {}), symbolFillColor: d.text.muted }; + neutralised = true; + }); + if (neutralised) { + say('legend.placement', + 'the size key is drawn in neutral ink — beside a colour key, swatches in series ink read as another category'); + } + } + + // A key to *values* is sampled, not enumerated: the renderer will happily + // print every tick it can fit, and a row of nine bubbles reads as data. The + // house says how many it is worth spending; the values are chosen round so + // the reader can interpolate between them. + if (l.maxSwatches) { + let capped = false; + walk(spec, (node) => { + for (const channel of ['size', 'color', 'opacity'] as const) { + const enc = node.encoding?.[channel]; + if (!enc?.field || enc.type !== 'quantitative' || enc.legend === null) continue; + if (channel === 'color' && !enc.scale?.type) continue; + if (enc.legend?.values) continue; + const values = roundSample(table, enc.field, l.maxSwatches!); + if (!values) continue; + enc.legend = { ...(enc.legend ?? {}), values }; + capped = true; + } + }); + if (capped) { + say('legend.maxSwatches', + `the key to values is sampled at ${l.maxSwatches} round sizes — a swatch for every tick reads as data, not as a key`); + } + } + + // Two keys, one row. Laid side by side above the plot a colour key and a + // size key eat the block between them, and past a point the second is + // pushed hard against the first with nothing to separate them. Vega will + // stack them given the word, so the word is given by measurement: they + // share a row while they fit across the block, and take one each when they + // do not. + if (l.orient === 'top' || l.orient === 'bottom') { + const widths = keyWidths(spec, table, l.label.fontSize ?? 10); + const block = blockWidth(spec, table); + const total = widths.reduce((a, b) => a + b, 0); + if (widths.length > 1 && block && total > block) { + config.legend.layout = { [l.orient]: { direction: 'vertical', anchor: 'start' } }; + say('legend.placement', + `${widths.length} keys want ${Math.round(total)}px across a ${block}px block — they take a row each`); + } + } + void say; +} + +/** + * Roughly how wide each key drawn on this chart wants to be. + * + * Rough is the point: the question is whether two keys fit on one row, and that + * is answered by tens of pixels, not by ones. A swatch, a gap, the label, the + * space to the next entry — summed over the entries the key will show. + */ +function keyWidths(spec: any, table: any[], fontSize: number): number[] { + const seen = new Map(); + walk(spec, (node) => { + for (const channel of ['color', 'fill', 'stroke', 'size', 'shape', 'opacity'] as const) { + const enc = node.encoding?.[channel]; + if (!enc?.field || enc.legend === null) continue; + // One field is one key. Vega-Lite merges the guides for a field + // however many channels and layers carry it — a scatter that fills + // and strokes by the same column draws one legend, not two — so + // counting them separately would stack a legend against itself. + const entries: string[] = enc.legend?.values + ? enc.legend.values.map((v: any) => (typeof v === 'number' ? v.toLocaleString('en-US') : String(v))) + : Array.isArray(enc.scale?.domain) + ? enc.scale.domain.map(String) + : orderedValues(table, enc.field).map(String); + if (!entries.length) continue; + // A size key's swatch is as wide as the biggest bubble it shows, and + // that is the whole reason it is there. + const area = channel === 'size' ? (enc.scale?.range?.[1] ?? 100) : 0; + const symbol = area ? 2 * Math.sqrt(area / Math.PI) : 10; + const width = entries.reduce((w, e) => w + symbol + 4 + e.length * fontSize * 0.55 + 10, 0); + seen.set(enc.field, Math.max(seen.get(enc.field) ?? 0, width)); + } + }); + return [...seen.values()]; +} + +/** A few round numbers spanning what a field holds, largest last. */ +function roundSample(table: any[], field: string, count: number): number[] | undefined { + let max = -Infinity; + let min = Infinity; + for (const row of table ?? []) { + const v = Number(row?.[field]); + if (!Number.isFinite(v)) continue; + if (v > max) max = v; + if (v < min) min = v; + } + if (!Number.isFinite(max) || max <= 0) return undefined; + const round1 = (v: number) => { + const mag = 10 ** Math.floor(Math.log10(Math.abs(v))); + return Math.round(v / mag) * mag; + }; + const out: number[] = []; + for (let i = 0; i < count; i++) { + const t = count === 1 ? 1 : (i + 1) / count; + const v = round1(min + (max - min) * t * t); + if (v > 0 && !out.includes(v)) out.push(v); + } + return out.length > 1 ? out : undefined; +} + +// --------------------------------------------------------------------------- +// Facet chrome +// --------------------------------------------------------------------------- + +function applyFacetChrome(config: any, d: DesignDecisions): void { + const f = d.facets; + config.header = { + ...(config.header ?? {}), + labelFont: f.header.font, + labelFontSize: f.header.fontSize, + labelColor: f.header.color, + labelFontWeight: f.header.fontWeight ?? 'normal', + ...(f.header.show ? {} : { labels: false }), + ...(f.header.fieldTitle ? {} : { title: null }), + }; + // A header is drawn *in* the gap above its panel. A house may set its + // panels tight — or inherit a tight layout — but the gap still has to hold + // the name, or the name lands on the panel above it. + const current = config.facet?.spacing; + const base = f.spacing ?? (typeof current === 'number' ? current : undefined); + const needed = f.header.show === false ? 0 : Math.round((f.header.fontSize ?? 11) * 1.7); + if (base != null && needed > base) { + config.facet = { ...(config.facet ?? {}), spacing: { row: needed, column: base } }; + } else if (f.spacing != null) { + config.facet = { ...(config.facet ?? {}), spacing: f.spacing }; + } +} + +/** + * A concat child may carry a title of its own. It is not the chart's headline + * — it names one panel of several — but Vega-Lite gives every title in the + * spec the same config, so it arrives in the headline's weight and the + * headline's anchor, competing with the sentence above it. + * + * A panel of a concat and a panel of a facet are the same thing to the reader, + * so the name is set in the voice the house already chose for facet headers, + * over the middle of the panel it names. + * + * And where that name is a value of the field the panels are coloured by, it + * is written in that value's ink. The name and the swatch become one object, + * which is the reason there is no key beside it. + */ +function applyPanelTitles(spec: any, d: DesignDecisions, say: (p: string, m: string) => void): void { + const f = d.facets; + let said = false; + const visit = (node: any): void => { + if (!node || typeof node !== 'object') return; + for (const key of ['hconcat', 'vconcat', 'concat']) { + const children = (node[key] ?? []).filter((c: any) => typeof c?.title === 'string'); + // The ink says which panel is which only when the panels differ in + // it. Where every panel is drawn in the same colour, that colour + // distinguishes nothing, and a name written in it is decoration. + const inks = children.map((c: any) => panelInk(c, c.title)); + const distinct = new Set(inks.filter(Boolean)); + const naming = distinct.size > 1 && distinct.size === children.length; + for (const [i, child] of children.entries()) { + const ink = naming ? inks[i] : undefined; + child.title = { + text: child.title, + anchor: 'middle', + font: f.header.font, + fontSize: f.header.fontSize, + fontWeight: f.header.fontWeight ?? 'normal', + color: ink ?? f.header.color, + }; + if (!said) { + say('facets.header', ink + ? 'the panel names are set in their own panel\'s ink — the name is the swatch, so no key is drawn beside it' + : 'the panel names are set in the header voice, not the headline\'s — they name a panel, not the chart'); + said = true; + } + } + for (const child of node[key] ?? []) visit(child); + } + if (node.spec) visit(node.spec); + }; + visit(spec); +} + +/** + * The ink a panel is drawn in: the colour its own scale gives the value it is + * named for, or — where the panel holds one series and the colour was painted + * straight onto the marks — that colour. Read off the chart rather than + * recomputed, since whatever ink ended up on the marks is the ink the reader + * will match the name to. + */ +function panelInk(node: any, value: string): string | undefined { + let scaled: string | undefined; + const painted = new Set(); + walk(node, (n) => { + for (const channel of ['color', 'fill', 'stroke'] as const) { + const enc = n.encoding?.[channel]; + if (!enc) continue; + const domain = enc.scale?.domain; + const range = enc.scale?.range; + if (Array.isArray(domain) && Array.isArray(range)) { + const i = domain.indexOf(value); + if (i >= 0 && typeof range[i] === 'string') scaled ??= range[i]; + } + if (typeof enc.value === 'string' && DATA_MARKS.has(markTypeOf(n.mark) ?? '')) painted.add(enc.value); + } + if (!DATA_MARKS.has(markTypeOf(n.mark) ?? '')) return; + const mark = normalizeMark(n.mark); + for (const key of ['color', 'fill'] as const) { + if (typeof mark[key] === 'string') painted.add(mark[key]); + } + }); + return scaled ?? (painted.size === 1 ? [...painted][0] : undefined); +} + +// --------------------------------------------------------------------------- +// Data labels — the first thing Vega-Lite has no primitive for +// --------------------------------------------------------------------------- + +function isStacked(node: any, measureChannel: 'x' | 'y'): boolean { + const mark = markTypeOf(node.mark); + if (mark !== 'bar' && mark !== 'area') return false; + const hasSeries = Boolean(node.encoding?.color?.field); + const dodged = Boolean(node.encoding?.xOffset || node.encoding?.yOffset); + const explicit = node.encoding?.[measureChannel]?.stack; + if (explicit === null || explicit === false) return false; + return hasSeries && !dodged; +} + +function applyDataLabels(spec: any, d: DesignDecisions, table: any[], say: (p: string, m: string) => void): any { + if (!d.dataLabels.show) return undefined; + + // A concatenation is several charts side by side. Labelling only the first + // panel is worse than labelling none. + const said = new Set(); + const once = (path: string, message: string) => { + const k = `${path}\u0000${message}`; + if (said.has(k)) return; + said.add(k); + say(path, message); + }; + let first: any; + for (const body of plotBodies(spec)) { + const layer = labelOneBody(spec, body, d, table, once); + if (layer && !first) first = layer; + } + return first; +} + +/** + * The ink for a number printed on a ramp. + * + * Vega-Lite cannot ask a mark what colour it ended up, so the places where the + * ramp goes dark are worked out here and restated as a test on the value. A + * sequential ramp darkens at one end and a diverging one at both, and the same + * scan covers either. + */ +function inkOnRamp(field: string, stops: string[], values: number[], light: string, dark: string): any { + const lo = Math.min(...values); + const hi = Math.max(...values); + if (!(hi > lo) || stops.length < 2) return { value: dark }; + const STEPS = 32; + const runs: Array<[number, number]> = []; + for (let i = 0; i <= STEPS; i++) { + const t = (i / STEPS) * (stops.length - 1); + const a = parseColor(stops[Math.floor(t)]); + const b = parseColor(stops[Math.min(stops.length - 1, Math.floor(t) + 1)]); + if (!a || !b) continue; + const k = t - Math.floor(t); + const c = { r: a.r + (b.r - a.r) * k, g: a.g + (b.g - a.g) * k, b: a.b + (b.b - a.b) * k, a: 1 }; + if (luminance(c) >= 0.5) continue; + const v = lo + (i / STEPS) * (hi - lo); + const last = runs[runs.length - 1]; + if (last && i > 0 && last[1] >= lo + ((i - 1) / STEPS) * (hi - lo)) last[1] = v; + else runs.push([v, v]); + } + if (runs.length === 0) return { value: dark }; + const f = `datum[${JSON.stringify(field)}]`; + const test = runs + .map(([a, b]) => (a === b ? `${f} === ${a}` : `(${f} >= ${a} && ${f} <= ${b})`)) + .join(' || '); + return { condition: { test, value: light }, value: dark }; +} + +function labelOneBody(spec: any, body: any, d: DesignDecisions, table: any[], say: (p: string, m: string) => void): any { + const units = body.layer + ? body.layer.filter((n: any) => DATA_MARKS.has(markTypeOf(n.mark) ?? '')) + : (markTypeOf(body.mark) ? [body] : []); + if (units.length === 0) { + say('dataLabels', 'no data mark found to label'); + return; + } + // A pie has no axis, but it does have a measure: the angle. That is where + // a value label matters most, since an angle is the hardest quantity to + // read off a chart. + const radial = units.some((n: any) => markTypeOf(n.mark) === 'arc'); + // A heat map keeps its quantity in colour and its position in the grid. + // The cell is the position, and the number belongs in the middle of it. + const cells = !radial + && d.bound.measureChannels.length === 0 + && units.every((n: any) => markTypeOf(n.mark) === 'rect'); + const measureChannel: 'x' | 'y' | 'theta' | 'color' = radial ? 'theta' + : cells ? 'color' + : d.bound.measureChannels[0]; + if (!measureChannel) { + say('dataLabels', 'no measure axis to label'); + return undefined; + } + if (body.layer && body.layer.some((n: any) => markTypeOf(n.mark) === 'text')) { + say('dataLabels', 'template already prints its own labels — left alone'); + return; + } + // A composite mark spends several layers saying one thing about each datum + // — open, high, low, close. There is no single number to print beside it, + // and printing one of the four would be a lie by selection. + const measureFields = new Set( + units.map((n: any) => mergedEncoding(n, body.encoding)[measureChannel]?.field).filter(Boolean), + ); + if (measureFields.size > 1) { + say('dataLabels', + `the mark carries ${measureFields.size} measures (${[...measureFields].join(', ')}) — no single value to print`); + return; + } + + const primary = units[0]; + const enc = mergedEncoding(primary, body.encoding); + const measure = enc[measureChannel]; + if (!measure?.field) { + say('dataLabels', `measure channel \`${measureChannel}\` has no field`); + return; + } + if (measureChannel !== 'theta' && measureChannel !== 'color' && isStacked(primary, measureChannel)) { + say('dataLabels', 'stacked segments are not labelled — Vega-Lite would place text at the segment edge'); + return; + } + + const horizontal = measureChannel === 'x'; + const outside = d.dataLabels.placement !== 'atMark'; + const t = d.dataLabels.text; + // `atMark` means "inside the mark" only when the mark has an inside. A bar + // or a slice has a body a label can sit on; a point or a line vertex does + // not, and a label there is over bare surface. + const BODY_MARKS = new Set(['bar', 'arc', 'rect', 'area']); + const onMarkBody = BODY_MARKS.has(markTypeOf(primary.mark) ?? ''); + + const markDef: any = { + type: 'text', + font: t.font, + fontSize: t.fontSize, + ...(t.fontWeight ? { fontWeight: t.fontWeight } : {}), + ...(t.fontStyle ? { fontStyle: t.fontStyle } : {}), + }; + const inside = !outside && onMarkBody; + // A reversed scale turns the mark round: the end of the bar is on the side + // the axis started from, so "outside" is the other way. + const reversed = measure.scale?.reverse === true; + // The offset is measured from the anchor, and the anchor is not always the + // edge of the mark. A bar's anchor is the end of it, so 4px of offset is + // 4px of air; a dot's anchor is its centre, and 4px from the centre of a + // 10px dot lands the text on the dot. Where a mark has a radius, the gap + // is measured from the rim by adding it. + const radius = Math.max(0, ...units.map((unit: any) => markRadius(unit, spec, d))); + const geometry = (within: boolean): any => { + const w = reversed ? !within : within; + return horizontal + ? { align: w ? 'right' : 'left', baseline: 'middle', dx: w ? -(5 + radius) : 4 + radius } + : { align: 'center', baseline: w ? 'top' : 'bottom', dy: w ? 4 + radius : -(4 + radius) }; + }; + if (cells) { + // A cell has no outside: the grid is continuous, and a number beside a + // cell belongs to no cell in particular. + Object.assign(markDef, { align: 'center', baseline: 'middle' }); + if (d.dataLabels.placement !== 'atMark') { + say('dataLabels.placement', + `\`${d.dataLabels.placement}\` printed in the cell instead — a grid is continuous and has no outside`); + } + } else if (radial) { + // On an arc the label rides the same angle as its slice; the only + // choice left is how far out along the radius it sits. Vega-Lite does + // not give a text mark the arc's radius, so it has to be stated — + // from the arc if it declares one, otherwise from the plot box, which + // is where Vega-Lite's own default comes from. + const arc = units.find((n: any) => markTypeOf(n.mark) === 'arc'); + const declared = normalizeMark(arc?.mark)?.outerRadius; + const w = body.width ?? spec.width; + const h = body.height ?? spec.height; + const r = typeof declared === 'number' + ? declared + : (typeof w === 'number' && typeof h === 'number' ? Math.min(w, h) / 2 : undefined); + if (r) Object.assign(markDef, { radius: inside ? r * 0.72 : r + 14 }); + else say('dataLabels', 'the arc has no radius to hang a label from'); + } else { + Object.assign(markDef, geometry(inside)); + } + if (d.dataLabels.placement === 'column' && !cells) { + say('dataLabels.placement', '`column` approximated as `outsideMark` — Vega-Lite has no label gutter'); + } + + const labelEncoding: any = { + text: { field: measure.field, type: 'quantitative', ...(d.dataLabels.format ? { format: d.dataLabels.format } : {}) }, + }; + if (d.dataLabels.unit) { + printWithUnit(body, { encoding: labelEncoding }, measure.field, d.dataLabels.format, d.dataLabels.unit); + say('annotation.unit', + `each printed value carries its unit \`${d.dataLabels.unit}\` — there is no axis left to state it on`); + } + for (const ch of ['x', 'y', 'xOffset', 'yOffset', 'column', 'row', 'facet', 'detail', 'theta', 'radius'] as const) { + if (enc[ch]) labelEncoding[ch] = stripAxis(enc[ch]); + } + // Vega-Lite stacks `theta` for arcs but not for text, so a copied angle + // would measure from zero instead of from the start of the slice. + if (radial && labelEncoding.theta) labelEncoding.theta = { ...labelEncoding.theta, stack: true }; + + if (cells) { + // The cell under the number is the ramp, so the ink follows the ramp. + const values = table.map((r) => r?.[measure.field]).filter((v) => typeof v === 'number'); + const stops = d.series.ramp?.stops ?? d.series.range ?? []; + labelEncoding.color = inkOnRamp(measure.field, stops, values, d.text.inverse, d.text.primary); + } else if (d.dataLabels.inkMode === 'matchSeries' && enc.color?.field) { + labelEncoding.color = { ...enc.color, legend: null }; + } else if (d.dataLabels.inkMode === 'contrastWithMark' && !outside && onMarkBody) { + const seriesInk = d.series.mode === 'single' ? d.series.single : d.series.categorical[0] ?? d.series.single; + markDef.color = readableOn(seriesInk, d.text.inverse, d.text.primary); + } else if (d.dataLabels.inkMode === 'contrastWithMark') { + // The label is floating over the surface, not sitting on the mark, so + // the thing it has to be readable against is the surface. + markDef.color = readableOn(d.surface.plot ?? d.surface.canvas, d.text.inverse, d.text.primary); + } else { + markDef.color = t.color ?? d.text.primary; + } + + const layer: any = { __themeSynthetic: true, mark: markDef, encoding: labelEncoding }; + appendLayer(body, layer); + + if (radial) { + // Vega-Lite gives a text mark the middle of a slice only when the + // angle is *shared* by the layers and declared stacked. The colour has + // to move up with it, because the stacking order is read off the + // colour field — left behind, the labels stack in a different order + // from the slices they belong to. The label then overrides the + // inherited colour with its own ink. + const arc = body.layer.find((n: any) => markTypeOf(n.mark) === 'arc'); + const shared: any = { ...(body.encoding ?? {}) }; + const theta = arc?.encoding?.theta ?? shared.theta; + if (theta) { + shared.theta = { ...theta, stack: true }; + if (arc?.encoding) delete arc.encoding.theta; + } + const colour = arc?.encoding?.color ?? shared.color; + if (colour?.field) { + shared.color = colour; + if (arc?.encoding) delete arc.encoding.color; + if (!labelEncoding.color) labelEncoding.color = { value: markDef.color ?? d.text.primary }; + // Vega-Lite derives the stacking order from the colour field. The + // label overrides that colour with its own ink, which would leave + // it stacking by value instead, so the order is stated outright + // and both layers read the same one. + shared.order = { field: colour.field, type: colour.type ?? 'nominal', sort: 'ascending' }; + } + body.encoding = shared; + delete labelEncoding.theta; + } + + // A label goes where there is room. A mark shorter than its own label + // cannot hold it, and a mark that reaches the end of the scale has no room + // past its end — so each case sends those few labels the other way. + // Vega-Lite has no conditional `align`, so this is two layers with + // complementary filters. + const flipInk = (within: boolean): string | undefined => { + if (d.dataLabels.inkMode === 'matchSeries' && enc.color?.field) return undefined; + if (!within) return t.color ?? d.text.primary; + const seriesInk = d.series.mode === 'single' ? d.series.single : d.series.categorical[0] ?? d.series.single; + return readableOn(seriesInk, d.text.inverse, d.text.primary); + }; + const split = (threshold: number, comparison: '<' | '>', message: string) => { + const v = `abs(datum[${JSON.stringify(measure.field)}])`; + const flipped = !inside; + layer.transform = [{ filter: `${v} ${comparison === '<' ? '>=' : '<='} ${threshold}` }]; + const other: any = { + __themeSynthetic: true, + transform: [{ filter: `${v} ${comparison} ${threshold}` }], + mark: { ...markDef, ...geometry(flipped), color: flipInk(flipped) }, + encoding: labelEncoding, + }; + if (other.mark.color === undefined) delete other.mark.color; + appendLayer(body, other); + say('dataLabels.placement', message); + }; + + if (!radial && !cells && onMarkBody) { + if (inside && d.dataLabels.insideMinValue != null) { + split(d.dataLabels.insideMinValue, '<', 'marks shorter than their own label print it outside instead'); + growPadding(spec, horizontal ? 'right' : 'top', (t.fontSize ?? 10) * 2); + } else if (!inside && d.dataLabels.outsideMaxValue != null) { + split(d.dataLabels.outsideMaxValue, '>', 'marks that reach the end of the scale print their label inside instead'); + } + } + + // Outside labels sit past the end of the mark, so the plot needs room. + if (!inside && horizontal) growPadding(spec, reversed ? 'left' : 'right', (t.fontSize ?? 10) * 3); + return layer; +} + +function stripAxis(enc: any): any { + const out = { ...enc }; + delete out.axis; + delete out.scale; + delete out.legend; + return out; +} + +function appendLayer(body: any, layer: any): void { + if (body.layer) { body.layer.push(layer); return; } + const base: any = { mark: body.mark }; + if (body.encoding) base.encoding = body.encoding; + delete body.mark; + delete body.encoding; + + // A unit spec with a `facet`, `row` or `column` channel is an operator in + // a unit's clothes: those channels split the data, and Vega-Lite refuses + // them inside a layer — it drops the split and draws every panel's marks + // on top of each other. So the split is promoted to a real facet operator + // and the layers go inside it. + const split = ['facet', 'row', 'column'].filter((c) => base.encoding?.[c]); + if (split.length === 0) { body.layer = [base, layer]; return; } + + const facet: any = {}; + for (const channel of split) { + const def = { ...base.encoding[channel] }; + delete base.encoding[channel]; + if (channel === 'facet') Object.assign(facet, def); + else facet[channel] = def; + } + const columns = body.columns ?? facet.columns; + delete facet.columns; + delete body.columns; + + const inner: any = { layer: [base, layer] }; + for (const key of ['width', 'height', 'view'] as const) { + if (body[key] !== undefined) { inner[key] = body[key]; delete body[key]; } + } + body.facet = facet; + if (columns != null) body.columns = columns; + body.spec = inner; +} + +function readableOn(background: string, light: string, dark: string): string { + const bg = parseColor(background); + if (!bg) return dark; + return luminance(bg) < 0.5 ? light : dark; +} + +function growPadding(spec: any, side: 'left' | 'right' | 'top' | 'bottom', amount: number): void { + const base = typeof spec.padding === 'number' + ? { left: spec.padding, right: spec.padding, top: spec.padding, bottom: spec.padding } + : { left: 8, right: 8, top: 8, bottom: 8, ...(spec.padding ?? {}) }; + base[side] = (base[side] ?? 8) + Math.round(amount); + spec.padding = base; +} + +// --------------------------------------------------------------------------- +// seriesEnd — the second thing Vega-Lite has no primitive for +// --------------------------------------------------------------------------- + +/** + * `seriesEnd` is a placement, and a placement can be unavailable. Decide that + * *before* the legend is drawn — once the colour legends have been suppressed + * in favour of end labels there is nothing to fall back to. + */ +function demoteSeriesEnd(spec: any, d: DesignDecisions, say: (p: string, m: string) => void): void { + if (d.legend.placement !== 'seriesEnd' && d.legend.placement !== 'inline') return; + if (!d.legend.show) return; + const body = plotBody(spec); + // A band carries its own end label inside itself, so it counts as a run + // with an end just as much as a line does. + const endable = (mark: string) => LINE_MARKS.has(mark) || mark === 'area'; + const units = body.layer + ? body.layer.filter((n: any) => endable(markTypeOf(n.mark) ?? '')) + : (endable(markTypeOf(body.mark) ?? '') ? [body] : []); + const enc = units.length ? mergedEncoding(units[0], body.encoding) : {}; + const field = enc.color?.field ?? d.bound.seriesField ?? enc.detail?.field; + // A name that will not fit inside its band goes in the margin past the last + // reading — and where the house has stood the value axis in that margin, as + // it does on a part-to-whole chart, the name would be printed over the + // ruler. A band is thin more often than not, so this is not a risk worth + // running: the naming goes back to the house's next choice. + const bands = units.some((n: any) => markTypeOf(n.mark) === 'area'); + const runsAlongX = runChannel(d) === 'x'; + const marginTaken = bands + && (runsAlongX ? d.axes.y?.orient === 'right' : d.axes.x?.orient === 'top'); + const reason = units.length === 0 + ? '`seriesEnd` needs a line mark' + : (!field ? '`seriesEnd` needs a series field to name' + : (field === d.bound.categoryField + ? '`seriesEnd` would restate the categorical axis' + : marginTaken + ? `the ${runsAlongX ? 'right' : 'top'} margin holds the value axis, so a name too big for its band has nowhere to stand` + : null)); + if (!reason) return; + // The house ranked its placements; a demotion should land on the next one + // it named, not on whatever this function happens to prefer. + const next = d.legend.fallbacks?.find((p) => p !== 'seriesEnd' && p !== 'inline') ?? 'right'; + say('legend.placement', `${reason} — the key is drawn \`${next}\` instead`); + d.legend.placement = next; + d.legend.orient = next === 'inside' ? 'top-right' : next as any; + d.legend.direction = next === 'top' || next === 'bottom' ? 'horizontal' : 'vertical'; +} + +/** + * The channel a series runs *along*. A measure on `y` means the run is `x`, + * a measure on `x` means the run is `y` — and when neither channel carries a + * measure the run is still the horizontal one, because a chart whose value is + * an *order* rather than a quantity (a bump chart ranks its rows) still reads + * left to right. Calling `y` the run in that case puts every series' label on + * the same row: at the top, on top of each other. + */ +function runChannel(d: DesignDecisions): 'x' | 'y' { + if (d.bound.measureChannels.includes('y')) return 'x'; + if (d.bound.measureChannels.includes('x')) return 'y'; + return 'x'; +} + +function applySeriesEndLabels( + spec: any, + d: DesignDecisions, + valueLayer: any, + table: any[], + say: (p: string, m: string) => void, +): void { + if (d.legend.placement !== 'seriesEnd' && d.legend.placement !== 'inline') return; + if (!d.legend.show) return; + + const body = plotBody(spec); + // A template that already prints a name at the end of each run has said it. + // A second label in the same place is not a legend, it is a stutter. + if ((body.layer ?? []).some((n: any) => markTypeOf(n.mark) === 'text' && n.encoding?.text?.field)) { + say('legend.placement', 'the chart already prints its own end labels — no second set drawn'); + return; + } + const units = body.layer + ? body.layer.filter((n: any) => LINE_MARKS.has(markTypeOf(n.mark) ?? '')) + : (LINE_MARKS.has(markTypeOf(body.mark) ?? '') ? [body] : []); + if (units.length === 0) { + // A band is not a line, but it has the same last reading — and more + // room to say it in, because the name can go *inside* the band where + // the reader is already looking. + if (bandEndLabels(spec, body, d, table, say)) return; + say('legend.placement', '`seriesEnd` needs a line mark — no legend drawn'); + return; + } + + const primary = units[0]; + const enc = mergedEncoding(primary, body.encoding); + // `detail` is a grouping key, not a series: on a dumbbell it is the very + // field the categorical axis already names, and printing it at the end of + // each connector says the same word twice. + const seriesField = enc.color?.field ?? d.bound.seriesField ?? enc.detail?.field; + const domainChannel = runChannel(d); + const valueChannel = domainChannel === 'x' ? 'y' : 'x'; + const domain = enc[domainChannel]; + const value = enc[valueChannel]; + if (!seriesField || !domain?.field || !value?.field) { + say('legend.placement', '`seriesEnd` needs a series field on a positional line — no legend drawn'); + return; + } + + const t = d.legend.label; + // The line that carries the shape need not be the layer that carries the + // colour — a dumbbell draws its connector plain and colours the dots. + let colourEnc = enc.color?.field ? enc.color : undefined; + if (!colourEnc) { + for (const unit of (body.layer ?? [body])) { + const c = unit.encoding?.color ?? unit.encoding?.fill ?? unit.encoding?.stroke; + if (c?.field === seriesField) { colourEnc = c; break; } + } + } + // "The end of the series" only means something when the domain is ordered — + // a time axis, a scale, or ordered categories. An unordered domain still + // carries one order, the one its rows arrived in, and where the run is + // horizontal that is enough: the reader finishes at the right-hand column + // and there is margin there to write the name in. A run down the page is a + // list, and a list is labelled at its head, inside the plot. + const runs = domain.type === 'quantitative' || domain.type === 'temporal' || domain.type === 'ordinal'; + const atEnd = runs || domainChannel === 'x'; + const rank = runs + ? [{ + window: [{ op: 'row_number', as: '__seriesEndRank' }], + sort: [{ field: domain.field, order: 'descending' }], + groupby: [seriesField], + }] + : atEnd + ? [ + { window: [{ op: 'row_number', as: '__dataOrder' }] }, + { + window: [{ op: 'row_number', as: '__seriesEndRank' }], + sort: [{ field: '__dataOrder', order: 'descending' }], + groupby: [seriesField], + }, + ] + : [{ window: [{ op: 'row_number', as: '__seriesEndRank' }], groupby: [seriesField] }]; + + // Both the series name and its final value want the same few pixels. The + // house answer is not to stack them but to say them once: "Japan 84.5". + const merged = Boolean(valueLayer) && valueLayer.encoding?.text?.field === value.field; + const transform: any[] = [...rank, { filter: 'datum.__seriesEndRank === 1' }]; + let textField = seriesField; + if (merged) { + const fmt = d.dataLabels.format; + const v = `datum[${JSON.stringify(value.field)}]`; + const shown = fmt ? `format(${v}, ${JSON.stringify(fmt)})` : `${v} + ''`; + transform.push({ + calculate: `datum[${JSON.stringify(seriesField)}] + ' ' + ${shown}`, + as: '__seriesEndLabel', + }); + textField = '__seriesEndLabel'; + // …and the value layer must stop printing the point that is now named. + valueLayer.transform = [...rank, { filter: 'datum.__seriesEndRank !== 1' }]; + say('legend.placement', + 'series name and final value merged into one label — they compete for the same space'); + } + + const labelLayer: any = { + __themeSynthetic: true, + transform, + mark: { + type: 'text', + align: domainChannel === 'x' ? 'left' : 'center', + baseline: domainChannel === 'x' ? 'middle' : 'bottom', + dx: domainChannel === 'x' ? 5 : 0, + dy: domainChannel === 'x' ? 0 : -5, + font: t.font, + fontSize: t.fontSize, + ...(t.fontWeight ? { fontWeight: t.fontWeight } : {}), + ...(t.fontStyle ? { fontStyle: t.fontStyle } : {}), + }, + encoding: { + [domainChannel]: stripAxis(domain), + [valueChannel]: stripAxis(value), + text: { field: textField, type: 'nominal' }, + ...(colourEnc?.field ? { color: { ...colourEnc, legend: null } } : {}), + }, + }; + appendLayer(body, labelLayer); + + // The labels live outside the plot rectangle; the canvas has to make room + // or Vega-Lite will draw them over whatever is next to the chart. At the + // head of a list they sit inside it and no room is needed. + if (atEnd) { + const longest = estimateLongestLabel(table, seriesField) + (merged ? 6 : 0); + growPadding(spec, domainChannel === 'x' ? 'right' : 'top', longest * (t.fontSize ?? 10) * 0.55 + 8); + } + say('legend.placement', '`seriesEnd` realized as a synthesized text layer at each series\' last point'); +} + +/** + * The same idea as a series-end label, for a chart made of bands. + * + * A stacked area has no end point to hang a name off, but it has something + * better: at its last reading the band is a shape with a middle, and a name + * knocked out of that middle is unambiguous in a way a legend swatch never is. + * Returns true when it drew them. + */ +function bandEndLabels( + spec: any, + body: any, + d: DesignDecisions, + table: any[], + say: (p: string, m: string) => void, +): boolean { + const units = body.layer + ? body.layer.filter((n: any) => markTypeOf(n.mark) === 'area') + : (markTypeOf(body.mark) === 'area' ? [body] : []); + if (units.length === 0) return false; + + const enc = mergedEncoding(units[0], body.encoding); + const seriesField = enc.color?.field ?? d.bound.seriesField; + const domainChannel = runChannel(d); + const valueChannel = domainChannel === 'x' ? 'y' : 'x'; + const domain = enc[domainChannel]; + const value = enc[valueChannel]; + if (!seriesField || !domain?.field || !value?.field) return false; + + // Where the middle of a band sits depends on everything stacked under it, + // and Vega-Lite already works that sum out for the band itself. Asking it + // for the same sum on the label layer — same series field, same stack — + // keeps label and band in step; a second, cleverer calculation here would + // only find new ways to disagree. The label's colour scale is the plot + // surface throughout, so the name is knocked out of the band it names + // rather than tinted by it. + const seriesCount = Math.max( + Array.isArray(enc.color?.scale?.domain) + ? enc.color.scale.domain.length + : orderedValues(table, seriesField).length, + 1, + ); + const knockedOut = enc.color?.field + ? { + color: { + ...enc.color, + scale: { ...(enc.color.scale ?? {}), range: Array(seriesCount).fill(d.surface.plot) }, + legend: null, + }, + } + : { detail: { field: seriesField, type: 'nominal' } }; + const inSeriesInk = enc.color?.field + ? { color: { ...enc.color, legend: null } } + : { detail: { field: seriesField, type: 'nominal' } }; + + // A name can only be knocked out of a band wide and thick enough to hold + // it. Where the band ends as a sliver — Oceania under four other + // continents — or climbs away from under its own label, the name goes + // outside the plot in its own ink instead. And a chart with half its names + // in and half out reads as a mistake: past one exception they all go out, + // where they line up as a single list. + const t = d.legend.label; + const along = domainChannel === 'x' ? (body.width ?? spec.width) : (body.height ?? spec.height); + const across = valueChannel === 'y' ? (body.height ?? spec.height) : (body.width ?? spec.width); + const order: string[] = Array.isArray(enc.color?.scale?.domain) + ? enc.color.scale.domain.map((s: any) => String(s)) + : orderedValues(table, seriesField); + const homeless = namesTheBandCannotHold({ + table, + domainField: domain.field, + domainType: domain.type, + seriesField, + valueField: value.field, + order, + stacked: value.stack !== null && value.stack !== false && value.stack !== 'none', + centred: value.stack === 'center', + alongPx: typeof along === 'number' ? along : 600, + acrossPx: typeof across === 'number' ? across : 254, + fontSize: t.fontSize ?? 10, + }); + const outside = homeless.length > 1 ? order : homeless; + const outsideList = JSON.stringify(outside); + const belongs = (inside: boolean) => + `indexof(${outsideList}, datum[${JSON.stringify(seriesField)}] + '') ${inside ? '<' : '>='} 0`; + + const fmt = d.dataLabels.format; + const v = `datum[${JSON.stringify(value.field)}]`; + const shown = fmt ? `format(${v}, ${JSON.stringify(fmt)})` : `${v} + ''`; + // A normalized stack redraws every band as a share of its column, and the + // axis is read in per cent. The field still holds what it always held — + // 9,420 TWh of coal — so printing it beside the name puts a number on the + // chart that the chart does not draw anywhere. The name goes alone. + const normalized = value.stack === 'normalize'; + const name = normalized + ? `datum[${JSON.stringify(seriesField)}] + ''` + : `datum[${JSON.stringify(seriesField)}] + ' ' + ${shown}`; + // Both layers keep every series, because a stacked layer stacks only the + // rows it is given: drop one and the rest slide off their own bands. What + // changes between them is whether the text says anything. + const endLayer = (inside: boolean): any => ({ + __themeSynthetic: true, + transform: [ + { + window: [{ op: 'row_number', as: '__bandEndRank' }], + sort: [{ field: domain.field, order: 'descending' }], + groupby: [seriesField], + }, + { filter: 'datum.__bandEndRank === 1' }, + { + calculate: outside.length ? `${belongs(inside)} ? ${name} : ''` : name, + as: '__bandEndLabel', + }, + ], + mark: { + type: 'text', + align: domainChannel !== 'x' ? 'center' : inside ? 'right' : 'left', + baseline: 'middle', + dx: domainChannel !== 'x' ? 0 : inside ? -6 : 6, + font: t.font, + fontSize: t.fontSize, + fontWeight: 'bold', + ...(inside ? { color: d.surface.plot } : {}), + }, + encoding: { + [domainChannel]: stripAxis(domain), + [valueChannel]: { ...stripAxis(value), bandPosition: 0.5 }, + text: { field: '__bandEndLabel', type: 'nominal' }, + ...(inside ? knockedOut : inSeriesInk), + }, + }); + if (outside.length < order.length) appendLayer(body, endLayer(true)); + if (outside.length) { + appendLayer(body, endLayer(false)); + const longest = Math.max(...outside.map((s) => s.length)) + 8; + growPadding(spec, domainChannel === 'x' ? 'right' : 'top', longest * (t.fontSize ?? 10) * 0.55 + 8); + say('legend.placement', + outside.length === order.length + ? 'the bands climb away from their own labels — the names sit outside the plot in series ink, as a list' + : `${outside.length === 1 ? 'one band is' : `${outside.length} bands are`} too thin at the end to hold a name — those sit outside the plot in their own ink`); + } + // Layers share their scales, so without this the label's colour scale is + // the band's and the name comes out tinted the colour of the shape it is + // sitting on. The label keeps the same *domain*, which is what puts it in + // the right band; only the ink is its own. + body.resolve = { + ...(body.resolve ?? {}), + scale: { ...(body.resolve?.scale ?? {}), color: 'independent' }, + }; + say('legend.placement', + '`seriesEnd` realized inside each band at its last reading — a name in the band beats a swatch beside the chart'); + return true; +} + +/** + * Which names cannot be knocked out of the band they belong to. + * + * A label is set in the middle of the band's last reading and written + * backwards from there, so the band has to be thick enough to hold the letters + * *and* still be under them a label's width back. On a stack that narrows + * towards its end — or one drawn about a centre line, where the whole pile + * rises as it grows — a name set in the last band can run clean off the shape + * it names and onto the page. + */ +function namesTheBandCannotHold(args: { + table: any[]; + domainField: string; + domainType?: string; + seriesField: string; + valueField: string; + order: string[]; + stacked: boolean; + centred: boolean; + alongPx: number; + acrossPx: number; + fontSize: number; +}): string[] { + const { table, domainField, domainType, seriesField, valueField, order } = args; + + const byKey = new Map }>(); + const keys: string[] = []; + let index = 0; + for (const row of table ?? []) { + const raw = row?.[domainField]; + index++; + if (raw == null) continue; + const key = String(raw); + let rec = byKey.get(key); + if (!rec) { + const n = raw instanceof Date ? raw.getTime() + : typeof raw === 'number' ? raw + : domainType === 'temporal' ? Date.parse(key) : index; + rec = { at: Number.isFinite(n) ? n : index, vals: new Map() }; + byKey.set(key, rec); + keys.push(key); + } + const v = Number(row?.[valueField]); + if (!Number.isFinite(v)) continue; + const s = String(row?.[seriesField]); + rec.vals.set(s, (rec.vals.get(s) ?? 0) + v); + } + const points = keys.map((k) => byKey.get(k)!).sort((a, b) => a.at - b.at); + if (points.length === 0) return []; + + // Segment bounds in value space, top-first, the way the stack is drawn. + const bounds = (vals: Map): Map => { + const out = new Map(); + if (!args.stacked) { + for (const s of order) out.set(s, [0, vals.get(s) ?? 0]); + return out; + } + const total = order.reduce((sum, s) => sum + (vals.get(s) ?? 0), 0); + let running = args.centred ? total / 2 : total; + for (const s of order) { + const v = vals.get(s) ?? 0; + out.set(s, [running - v, running]); + running -= v; + } + return out; + }; + + const biggest = Math.max(...points.map((p) => order.reduce((sum, s) => sum + (p.vals.get(s) ?? 0), 0))); + if (!(biggest > 0)) return []; + const perPx = biggest / args.acrossPx; + const stepPx = args.alongPx / Math.max(1, points.length - 1); + const margin = (args.fontSize / 2 + 1) * perPx; + + const last = bounds(points[points.length - 1].vals); + const homeless: string[] = []; + for (const s of order) { + const here = last.get(s); + if (!here) continue; + const centre = (here[0] + here[1]) / 2; + // Roughly what the label measures: the name, a space, and a number. + const width = (s.length + 6) * args.fontSize * 0.55; + const back = Math.min(points.length - 1, Math.ceil(width / Math.max(stepPx, 1))); + for (let i = points.length - 1; i >= points.length - 1 - back; i--) { + const seg = bounds(points[i].vals).get(s); + if (!seg || centre < seg[0] + margin || centre > seg[1] - margin) { + homeless.push(s); + break; + } + } + } + return homeless; +} + +/** Distinct values of a field, in the order the table first shows them. */ +function orderedValues(table: any[], field: string): string[] { + const seen: string[] = []; + const set = new Set(); + for (const row of table ?? []) { + const v = row?.[field]; + if (v == null) continue; + const s = String(v); + if (!set.has(s)) { set.add(s); seen.push(s); } + } + return seen; +} + +function estimateLongestLabel(table: any[], field: string): number { + if (!Array.isArray(table)) return 10; + let max = 0; + for (const row of table) { + const v = row?.[field]; + if (v != null) max = Math.max(max, String(v).length); + } + return max || 10; +} + +// --------------------------------------------------------------------------- +// Point emphasis +// --------------------------------------------------------------------------- + +/** + * A dot on a line is a full stop: it says *this* is the value the sentence was + * building to. Houses differ on where it goes — both ends of the run, the + * latest reading, the high and the low — but they agree that it goes on a + * line, and only where the line does not already show every observation. + */ +function applyPointEmphasis(spec: any, d: DesignDecisions, say: (p: string, m: string) => void): void { + const policy = d.pointEmphasis; + if (!policy) return; + + const body = plotBody(spec); + const units = (body.layer ?? [body]).filter((n: any) => LINE_MARKS.has(markTypeOf(n.mark) ?? '')); + if (units.length === 0) return; + + // A line drawn with a point at every observation has already said it — + // whether the house asked for that or the chart spec did. + const drawsEveryPoint = d.marks.point?.show === true + || units.some((n: any) => n.mark && typeof n.mark === 'object' && n.mark.point); + if (drawsEveryPoint) { + say('annotation.pointEmphasis', + 'the line already shows every observation — a second dot at the end would say nothing new'); + return; + } + + const enc = mergedEncoding(units[0], body.encoding); + const domainChannel = runChannel(d); + const valueChannel = domainChannel === 'x' ? 'y' : 'x'; + const domain = enc[domainChannel]; + const value = enc[valueChannel]; + if (!domain?.field || !value?.field) return; + + const seriesField = enc.color?.field ?? d.bound.seriesField ?? enc.detail?.field; + const groupby = seriesField ? [seriesField] : []; + const v = `datum[${JSON.stringify(value.field)}]`; + + let transform: any[]; + let what: string; + if (policy.where === 'extremes') { + transform = [ + { + joinaggregate: [ + { op: 'min', field: value.field, as: '__peLow' }, + { op: 'max', field: value.field, as: '__peHigh' }, + ], + groupby, + }, + { filter: `${v} === datum.__peLow || ${v} === datum.__peHigh` }, + ]; + what = 'the high and the low of each run'; + } else { + // Row numbers, not values: the domain may be a date, and two dates are + // never `===` each other in a Vega expression even when they name the + // same instant. + const rank = (order: 'ascending' | 'descending', as: string) => ({ + window: [{ op: 'row_number', as }], + sort: [{ field: domain.field, order }], + groupby, + }); + transform = policy.where === 'latest' + ? [rank('descending', '__peLast'), { filter: 'datum.__peLast === 1' }] + : [ + rank('ascending', '__peFirst'), + rank('descending', '__peLast'), + { filter: 'datum.__peFirst === 1 || datum.__peLast === 1' }, + ]; + what = policy.where === 'latest' ? 'the latest reading' : 'the first and last reading of each run'; + } + + // The colour encoding is repeated, not re-declared: in a layered spec the + // scales are shared, and a `legend: null` on any one layer takes the key + // away from all of them. + const colourEnc = enc.color?.field ? { ...enc.color } : undefined; + appendLayer(body, { + __themeSynthetic: true, + transform, + mark: { + type: 'point', + filled: true, + size: policy.size, + ...(d.marks.point?.haloColor + ? { stroke: d.marks.point.haloColor, strokeWidth: d.marks.point.haloWidth ?? 1 } + : {}), + ...(colourEnc ? {} : { color: d.series.single }), + }, + encoding: { + [domainChannel]: stripAxis(domain), + [valueChannel]: stripAxis(value), + ...(colourEnc ? { color: colourEnc } : {}), + }, + }); + say('annotation.pointEmphasis', `${what} carries a dot — the house marks where the line lands`); + + // Vega-Lite sizes a legend swatch from the largest mark on the scale, so + // the dot just added would swell every swatch in the key. The key samples + // the line, not the full stop at the end of it. + if (colourEnc && d.legend.show) { + const legendConfig = ((spec.config ??= {}).legend ??= {}); + if (legendConfig.symbolSize == null) { + legendConfig.symbolSize = ((d.legend.label.fontSize ?? 10) * 0.8) ** 2; + } + } + + if (policy.labels === 'never') return; + const t = d.dataLabels.text; + appendLayer(body, { + __themeSynthetic: true, + ...(policy.labels === 'endpoints' ? { transform } : {}), + mark: { + type: 'text', + align: domainChannel === 'x' ? 'center' : 'left', + baseline: domainChannel === 'x' ? 'bottom' : 'middle', + dy: domainChannel === 'x' ? -8 : 0, + dx: domainChannel === 'x' ? 0 : 8, + font: t.font, + fontSize: t.fontSize, + ...(t.fontWeight ? { fontWeight: t.fontWeight } : {}), + fill: t.color, + }, + encoding: { + [domainChannel]: stripAxis(domain), + [valueChannel]: stripAxis(value), + text: { + field: value.field, + type: 'quantitative', + ...(d.dataLabels.format ? { format: d.dataLabels.format } : {}), + }, + }, + }); + say('annotation.pointLabels', + `${policy.labels === 'all' ? 'every point' : 'the dotted points'} print their value`); +} + +// --------------------------------------------------------------------------- +// Statistics +// --------------------------------------------------------------------------- + +/** The fit a template asked the renderer for, if it asked for one. */ +function findFit(spec: any): { y: string; x: string } | undefined { + let found: { y: string; x: string } | undefined; + walk(spec, (node) => { + if (found || !Array.isArray(node.transform)) return; + for (const t of node.transform) { + const on = t?.on; + const y = t?.regression ?? t?.loess; + // A fit per group states several slopes; one caption cannot. + if (typeof y === 'string' && typeof on === 'string' && !t.groupby?.length) { + found = { y, x: on }; + return; + } + } + }); + return found; +} + +function formatStat(v: number): string { + const abs = Math.abs(v); + const text = abs >= 100 ? v.toFixed(0) : abs >= 1 ? v.toFixed(2) : v.toFixed(3); + return text.replace('-', '\u2212'); +} + +/** + * Print what the fit is worth. + * + * A house that draws a regression line and says nothing about it is asking the + * reader to take the line on trust. Where the house asks for the numbers, they + * are computed from the same rows the line was fitted to. + */ +function applyStatistics(spec: any, d: DesignDecisions, table: any[], say: (p: string, m: string) => void): void { + const policy = d.statistics; + if (!policy || !Array.isArray(spec.layer) || !Array.isArray(table) || table.length === 0) return; + const fit = findFit(spec); + if (!fit) return; + + const pairs = table + .map((row) => [Number(row?.[fit.x]), Number(row?.[fit.y])]) + .filter(([x, y]) => Number.isFinite(x) && Number.isFinite(y)); + if (pairs.length < 3) return; + + const n = pairs.length; + const mx = pairs.reduce((s, p) => s + p[0], 0) / n; + const my = pairs.reduce((s, p) => s + p[1], 0) / n; + let sxy = 0; let sxx = 0; let syy = 0; + for (const [x, y] of pairs) { + sxy += (x - mx) * (y - my); + sxx += (x - mx) ** 2; + syy += (y - my) ** 2; + } + if (sxx === 0 || syy === 0) return; + const slope = sxy / sxx; + const stats: Record = { + slope: `slope = ${formatStat(slope)}`, + intercept: `intercept = ${formatStat(my - slope * mx)}`, + r2: `R² = ${((sxy ** 2) / (sxx * syy)).toFixed(2)}`, + n: `n = ${n}`, + }; + const parts = policy.show.map((k) => stats[k]).filter(Boolean); + if (!parts.length) return; + + const caption = policy.placement === 'caption'; + spec.layer.push({ + __themeSynthetic: true, + data: { values: [{}] }, + mark: { + type: 'text', + text: parts.join(' · '), + align: caption ? 'left' : 'right', + baseline: 'bottom', + x: { expr: caption ? '0' : 'width' }, + y: { expr: caption ? 'height + 34' : '-4' }, + font: policy.font, + fontSize: policy.fontSize, + fontStyle: policy.fontStyle, + fill: policy.color, + }, + }); + say('annotation.statistics', + `the fit is stated as well as drawn — ${parts.join(', ')} — computed from the ${n} rows the line was fitted to`); +} + +// --------------------------------------------------------------------------- +// Furniture +// --------------------------------------------------------------------------- + +function applyFurniture(spec: any, d: DesignDecisions, table: any[], say: (p: string, m: string) => void): boolean { + if (!d.furniture.length) return false; + if (spec.vconcat || spec.hconcat || spec.concat) { + say('furniture', 'not drawn — the chart is already a concatenation'); + return false; + } + + const before: any[] = []; + const after: any[] = []; + // A tab is a mark of its own — the Economist's red rectangle is 26px + // because 26px is what it is. A rule is not: it closes the block, so its + // length is the block's, and a house that draws one does not state a + // number for it. Falling back to a stub leaves a dash in the corner that + // looks like a mistake rather than an edge. + const block = blockWidth(spec, table); + for (const item of d.furniture) { + const isRule = item.kind !== 'mastheadTab'; + const width = item.width ?? (isRule ? block : 40); + if (width == null) { + say('furniture', `the house draws a ${item.kind} across the block, but the chart states no width to draw it across — left out`); + continue; + } + if (isRule && item.width == null) { + say('furniture', `the ${item.kind} runs the width of the block — ${width}px — not a fixed stub`); + } + const isTop = (item.anchor ?? 'topLeft').startsWith('top'); + const rect = { + __themeSynthetic: true, + mark: { type: 'rect', color: item.color ?? d.text.primary }, + width, + height: item.height ?? 2, + data: { values: [{}] }, + }; + (isTop ? before : after).push(rect); + } + if (!before.length && !after.length) return false; + + const inner: any = { ...spec }; + for (const key of ['$schema', 'background', 'padding', 'title', 'config', 'autosize']) delete inner[key]; + + // A concatenated child's legends are hoisted to the outer view and drawn + // above every child in it — including the tab, which is the one thing that + // is supposed to open the block. Resolved independently they stay with the + // plot they key, and the house's rule sits back under the headline where it + // was drawn. + const keyed = new Set(); + walk(inner, (node) => { + for (const channel of ['color', 'fill', 'stroke', 'size', 'shape', 'opacity'] as const) { + const enc = node.encoding?.[channel]; + if (enc?.field && enc.legend !== null) keyed.add(channel); + } + }); + + const outer: any = { + ...(spec.$schema ? { $schema: spec.$schema } : {}), + background: spec.background, + padding: spec.padding, + ...(spec.title ? { title: spec.title } : {}), + spacing: 6, + vconcat: [...before, inner, ...after], + ...(keyed.size + ? { resolve: { legend: Object.fromEntries([...keyed].map((c) => [c, 'independent'])) } } + : {}), + config: spec.config, + }; + for (const key of Object.keys(spec)) delete spec[key]; + Object.assign(spec, outer); + return true; +} + +function clamp(v: number, lo: number, hi: number): number { + return Math.max(lo, Math.min(hi, v)); +} + +/** + * How wide the drawn block is, in the units furniture is measured in. The + * plotting rectangle is the only width the spec states; the labels down its + * left sit outside it, so a rule that closes the block is drawn a little past + * the plot rather than flush with it. + * + * A banded chart states no total at all — it states a step and lets the + * categories decide — so the total is read back the same way the layout wrote + * it, one band per name. + */ +function blockWidth(spec: any, table: any[]): number | undefined { + let widest: number | undefined; + const consider = (w: number | undefined) => { + if (w != null && Number.isFinite(w) && (widest == null || w > widest)) widest = w; + }; + // The step may be stated on a node whose children carry the encoding, so + // the band field is looked for down the branch, not only on the node. + const bandField = (node: any): string | undefined => { + let found: string | undefined; + walk(node, (n) => { + const enc = n.encoding?.x; + if (!found && enc?.field && (enc.type === 'nominal' || enc.type === 'ordinal')) found = enc.field; + }); + return found; + }; + walk(spec, (node) => { + const w = node.width; + if (typeof w === 'number') consider(w); + else if (w && typeof w === 'object' && typeof w.step === 'number') { + const count = distinctCount(table, bandField(node)); + if (count) consider(w.step * count); + } + }); + if (typeof spec.width === 'number') consider(spec.width); + // A chart that states no width of its own is drawn at the one the layout + // put in the view config — that is the width, not a default to guess at. + if (widest == null) consider(spec.config?.view?.continuousWidth); + return widest == null ? undefined : Math.round(widest * 1.07); +} + +void contrastingInk; diff --git a/packages/flint-js/tests/heatmap-colors.test.ts b/packages/flint-js/tests/heatmap-colors.test.ts index 0c29a623..d4915552 100644 --- a/packages/flint-js/tests/heatmap-colors.test.ts +++ b/packages/flint-js/tests/heatmap-colors.test.ts @@ -90,4 +90,69 @@ describe('heatmap color defaults', () => { expect(option.color).toBeUndefined(); expect(option.series[0].itemStyle?.color).toBeUndefined(); }); +}); + +/** + * A pivot is a question, not a fact about the numbers. The same temperatures + * split at freezing if we are asking what ices over, and somewhere near room + * temperature if we are asking where is pleasant to live; nothing in the data + * distinguishes the two, so the author says which. + */ +const CITY_TEMPS = [ + { city: 'Singapore', month: 'Jan', temp: 26 }, + { city: 'Singapore', month: 'Jul', temp: 27 }, + { city: 'Moscow', month: 'Jan', temp: -9 }, + { city: 'Moscow', month: 'Jul', temp: 19 }, +]; + +function tempHeatmapInput(annotation: Record) { + return { + data: { values: CITY_TEMPS }, + semantic_types: { + city: 'Category', + month: 'Month', + temp: { semanticType: 'Quantity', ...annotation }, + }, + chart_spec: { + chartType: 'Heatmap', + encodings: { + x: { field: 'month' }, + y: { field: 'city' }, + color: { field: 'temp' }, + }, + }, + } as any; +} + +describe('what a diverging colour scale pivots on', () => { + it('pivots on the value the author names, not on zero', () => { + const spec = assembleVegaLite(tempHeatmapInput({ divergingMidpoint: 18 })) as any; + + expect(spec.encoding.color.scale.domainMid).toBe(18); + // Symmetric about the pivot, or a degree above reads differently from a + // degree below: 18 - (-9) = 27 is the longer reach, so both arms take it. + expect(spec.encoding.color.scale.domain).toEqual([-9, 45]); + }); + + it('splits on the named value even when every reading is on one side of it', () => { + const warm = CITY_TEMPS.filter(r => r.temp > 0); + const spec = assembleVegaLite({ + ...tempHeatmapInput({ divergingMidpoint: 18 }), + data: { values: warm }, + }) as any; + + expect(spec.encoding.color.scale.domainMid).toBe(18); + }); + + it('falls back to zero when the author names nothing', () => { + const spec = assembleVegaLite(tempHeatmapInput({})) as any; + + expect(spec.encoding.color.scale.domainMid).toBe(0); + }); + + it('warms the high end of a measure whose sign carries no loss', () => { + const spec = assembleVegaLite(tempHeatmapInput({})) as any; + + expect(spec.encoding.color.scale.scheme).toBe('blueorange'); + }); }); \ No newline at end of file diff --git a/packages/flint-js/tests/theme-legend-rows.test.ts b/packages/flint-js/tests/theme-legend-rows.test.ts new file mode 100644 index 00000000..3ad6c311 --- /dev/null +++ b/packages/flint-js/tests/theme-legend-rows.test.ts @@ -0,0 +1,90 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { describe, it, expect } from 'vitest'; +import { assembleVegaLite } from '../src'; +import type { ThemeSpec } from '../src/core/theme/types'; + +/** + * A chart can need more than one key: one that names the colours and one that + * measures the sizes. Laid side by side above the plot they eat the block + * between them, and past a point the second is pushed against the first. + * + * Where they go is therefore a measurement, not a preference. + */ + +const rows = (n: number) => + Array.from({ length: n }, (_, i) => ({ + Country: `Country ${i}`, + Region: ['Europe', 'Americas', 'Asia', 'Africa'][i % 4], + GDP: 1000 * (i + 1), + Life: 60 + i, + Population: 10 * (i + 1), + })); + +const theme = (extra: Partial = {}): ThemeSpec => ({ + id: 'house', + label: 'House', + ink: { + surface: { canvas: '#ffffff' }, + series: { single: '#333333', categorical: ['#1', '#2', '#3', '#4'] }, + }, + legend: { show: 'always', placement: ['top'], direction: 'horizontal' }, + ...extra, +} as ThemeSpec); + +function bubble(width: number, sizeField: string | null = 'Population'): any { + return assembleVegaLite({ + data: { values: rows(8) }, + semantic_types: { + Country: 'Country', Region: 'Category', + GDP: 'Amount', Life: 'Quantity', Population: 'Quantity', + }, + chart_spec: { + chartType: 'Scatter Plot', + title: 'Money buys years', + encodings: { + x: 'GDP', + y: 'Life', + color: 'Region', + ...(sizeField ? { size: sizeField } : {}), + }, + baseSize: { width, height: 240 }, + }, + theme_spec: theme(), + } as any) as any; +} + +const layoutOf = (spec: any) => spec.config?.legend?.layout; + +describe('two keys above one plot', () => { + it('gives each a row when they will not fit across the block', () => { + const spec = bubble(320); + expect(layoutOf(spec)?.top?.direction).toBe('vertical'); + expect(spec._theme.report.some((r: any) => /row each/.test(r.message))).toBe(true); + }); + + it('leaves them on one row when the block is wide enough', () => { + const spec = bubble(1200); + expect(layoutOf(spec)).toBeUndefined(); + }); + + it('says nothing about rows when there is only one key', () => { + const spec = bubble(320, null); + expect(layoutOf(spec)).toBeUndefined(); + }); + + /** + * A size key inherits the mark's ink. Beside a colour key that is the ink + * of the first category, which makes the row read as one more of them. + */ + it('draws the size key in neutral ink beside a colour key', () => { + const spec = bubble(320); + const found: any[] = []; + JSON.stringify(spec, (_k, v) => { + if (v?.symbolFillColor) found.push(v.symbolFillColor); + return v; + }); + expect(found.length).toBeGreaterThan(0); + }); +}); diff --git a/packages/flint-js/tests/theme-presets.test.ts b/packages/flint-js/tests/theme-presets.test.ts new file mode 100644 index 00000000..640aca99 --- /dev/null +++ b/packages/flint-js/tests/theme-presets.test.ts @@ -0,0 +1,599 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { describe, it, expect } from 'vitest'; +import { assembleVegaLite } from '../src'; +import { markTypeOf } from '../src/vegalite/theme'; +import { THEME_PRESETS, listThemePresets, resolveThemeSpec } from '../src/core/theme/presets'; +import type { ThemeSpec } from '../src/core/theme/types'; + +/** + * A house may hold preferences about how a chart is *built*, not only how it + * looks: which options a chart type takes by default, and what the compiler + * should assume about size and stretch. + * + * Both are the middle of three levels. What the caller wrote in the chart spec + * wins; the house comes next; flint's own defaults come last. + */ + +const DATA = [ + { Year: 2019, Sales: 12 }, + { Year: 2020, Sales: 18 }, + { Year: 2021, Sales: 15 }, + { Year: 2022, Sales: 24 }, +]; + +function theme(extra: Partial): ThemeSpec { + return { + id: 'house', + label: 'House', + ink: { surface: { canvas: '#ffffff' }, series: { single: '#333333' } }, + ...extra, + } as ThemeSpec; +} + +function build(themeSpec: ThemeSpec, chartProperties?: Record) { + return assembleVegaLite({ + data: { values: DATA }, + semantic_types: { Year: 'Year', Sales: 'Quantity' }, + chart_spec: { + chartType: 'Line Chart', + encodings: { x: 'Year', y: 'Sales' }, + ...(chartProperties ? { chartProperties } : {}), + }, + theme_spec: themeSpec, + } as any) as any; +} + +function markOf(spec: any): any { + const node = spec.layer?.find((l: any) => l.mark && !l.__themeSynthetic) ?? spec; + return typeof node.mark === 'string' ? { type: node.mark } : node.mark; +} + +describe('naming a house Flint ships', () => { + it('reads the same as passing that house in full', () => { + const named = build('economist' as any); + const spelled = build(THEME_PRESETS.economist.spec); + expect(named._theme?.id).toBe('economist'); + expect(JSON.stringify(named)).toBe(JSON.stringify(spelled)); + }); + + it('says so when the name is not one of ours', () => { + expect(() => build('the-guardian' as any)).toThrow(/economist/); + }); + + it('lists every house it can resolve', () => { + for (const { id } of listThemePresets()) { + expect(resolveThemeSpec(id)).toBe(THEME_PRESETS[id].spec); + } + }); + + /** + * The guidance names a number of colours, and a number in prose drifts the + * moment the palette beside it changes. It is the same number, so it has to + * stay the same number — however the house chooses to word it. + */ + it('states a colour count the house actually declares', () => { + for (const preset of Object.values(THEME_PRESETS)) { + const line = preset.guidance.split('\n') + .find(l => /colour|key/i.test(l)); + const stated = line && /(\d+)/.exec(line); + expect(stated, `${preset.id} says nothing about colour`).toBeTruthy(); + const series = preset.spec.ink.series as any; + const declared = preset.spec.legend?.maxSwatches + ?? (series.categorical as string[]).length; + expect(Number(stated![1]), `${preset.id}`).toBe(declared); + } + }); +}); + +describe('theme chartDefaults', () => { + it('applies a house option the caller did not state', () => { + const spec = build(theme({ chartDefaults: { 'Line Chart': { showPoints: true } } })); + expect(markOf(spec).point).toBeTruthy(); + }); + + it('yields to the same option stated in the chart spec', () => { + const spec = build( + theme({ chartDefaults: { 'Line Chart': { showPoints: true } } }), + { showPoints: false }, + ); + expect(markOf(spec).point).toBeFalsy(); + expect(JSON.stringify(spec._theme?.report ?? [])).toContain('showPoints'); + }); + + it('drops an option the chart type does not declare, and says so', () => { + const spec = build(theme({ chartDefaults: { '*': { notAnOption: 3 } } })); + expect(JSON.stringify(spec._theme?.report ?? [])).toContain('notAnOption'); + }); +}); + +describe('theme compileDefaults', () => { + it('supplies a base size the chart spec left open', () => { + const wide = build(theme({ compileDefaults: { baseSize: { width: 640, height: 400 } } })); + const narrow = build(theme({ compileDefaults: { baseSize: { width: 300, height: 400 } } })); + expect(wide._width).toBeGreaterThan(narrow._width); + }); + + it('yields to a base size the chart spec states', () => { + const stated = (themeSpec: ThemeSpec) => assembleVegaLite({ + data: { values: DATA }, + semantic_types: { Year: 'Year', Sales: 'Quantity' }, + chart_spec: { + chartType: 'Line Chart', + encodings: { x: 'Year', y: 'Sales' }, + baseSize: { width: 300, height: 200 }, + }, + theme_spec: themeSpec, + } as any) as any; + const housePrefersWide = stated(theme({ compileDefaults: { baseSize: { width: 900, height: 700 } } })); + const houseSaysNothing = stated(theme({})); + expect(housePrefersWide._width).toBe(houseSaysNothing._width); + }); +}); + +describe('theme annotation.unitsInAxisTitle', () => { + const withUnit = (themeSpec: ThemeSpec) => assembleVegaLite({ + data: { values: DATA }, + semantic_types: { + Year: 'Year', + Sales: { semanticType: 'Amount', unit: 'kg' }, + }, + chart_spec: { chartType: 'Line Chart', encodings: { x: 'Year', y: 'Sales' } }, + theme_spec: themeSpec, + } as any) as any; + + function yTitle(spec: any): unknown { + const node = spec.layer?.find((l: any) => l.encoding?.y) ?? spec; + return node.encoding?.y?.axis?.title; + } + + it('states the unit in the title where the house keeps titles', () => { + const spec = withUnit(theme({ + annotation: { axisTitles: 'always', unitsInAxisTitle: true }, + })); + expect(yTitle(spec)).toBe('Sales (kg)'); + }); + + it('leaves the title alone where the house has no such rule', () => { + const spec = withUnit(theme({ annotation: { axisTitles: 'always' } })); + expect(yTitle(spec)).not.toBe('Sales (kg)'); + }); +}); + +describe('theme annotation.pointEmphasis', () => { + const emphasised = (spec: any) => (spec.layer ?? []).filter( + (l: any) => l.__themeSynthetic && markTypeOf(l.mark) === 'point', + ); + + it('dots the latest reading on a line', () => { + const spec = build(theme({ annotation: { pointEmphasis: 'latest' } })); + const dots = emphasised(spec); + expect(dots).toHaveLength(1); + expect(JSON.stringify(dots[0].transform)).toContain('row_number'); + }); + + it('says nothing where the line already shows every point', () => { + const spec = build( + theme({ annotation: { pointEmphasis: 'latest' } }), + { showPoints: true }, + ); + expect(emphasised(spec)).toHaveLength(0); + expect(JSON.stringify(spec._theme?.report ?? [])).toContain('already shows every observation'); + }); +}); + +describe('a printed value carries the unit no axis can', () => { + const SHARES = [ + { Browser: 'Chrome', Share: 65 }, + { Browser: 'Safari', Share: 20 }, + { Browser: 'Edge', Share: 10 }, + { Browser: 'Other', Share: 5 }, + ]; + const pie = (extra: Partial) => assembleVegaLite({ + data: { values: SHARES }, + semantic_types: { Browser: 'Category', Share: 'Quantity' }, + chart_spec: { chartType: 'Pie Chart', encodings: { size: 'Share', color: 'Browser' } }, + theme_spec: theme({ + dataLabels: { show: 'always', placement: 'atMark', inkMode: 'fixed' }, + ...extra, + }), + } as any) as any; + + function labelText(spec: any): any { + const walk = (node: any, owner: any): any => { + if (!node || typeof node !== 'object') return undefined; + if (markTypeOf(node.mark) === 'text') return { text: node.encoding?.text, transform: owner?.transform }; + for (const key of ['layer', 'vconcat', 'hconcat', 'concat'] as const) { + for (const child of node[key] ?? []) { + const found = walk(child, node); + if (found) return found; + } + } + return node.spec ? walk(node.spec, node.spec) : undefined; + }; + return walk(spec, spec) ?? {}; + } + + it('appends the unit to each slice value', () => { + const spec = pie({ annotation: { unit: 'lastTick' } }); + const { text, transform } = labelText(spec); + expect(text?.field).toBe('__flintValueWithUnit'); + expect(JSON.stringify(transform)).toContain('%'); + }); + + it('leaves the value bare where the house asks for no unit', () => { + const { text } = labelText(pie({})); + expect(text?.field).toBe('Share'); + }); +}); + +describe('a cell in a grid is a position', () => { + const GRID = [ + { City: 'Cairo', Month: 'Jan', Temp: 14 }, + { City: 'Cairo', Month: 'Feb', Temp: 16 }, + { City: 'Oslo', Month: 'Jan', Temp: -4 }, + { City: 'Oslo', Month: 'Feb', Temp: -2 }, + ]; + const heatmap = (themeSpec: ThemeSpec) => assembleVegaLite({ + data: { values: GRID }, + semantic_types: { City: 'Category', Month: 'Category', Temp: 'Quantity' }, + chart_spec: { chartType: 'Heatmap', encodings: { x: 'Month', y: 'City', color: 'Temp' } }, + theme_spec: themeSpec, + } as any) as any; + + it('prints the value in the cell, in ink that follows the ramp', () => { + const spec = heatmap(theme({ + dataLabels: { show: 'always', placement: 'atMark', inkMode: 'contrastWithMark' }, + ink: { + surface: { canvas: '#ffffff' }, + series: { single: '#333333', sequential: { stops: ['#eef3f8', '#051c2c'] } }, + }, + } as Partial)); + const label = (spec.layer ?? []).find( + (l: any) => l.__themeSynthetic && markTypeOf(l.mark) === 'text', + ); + expect(label).toBeTruthy(); + expect(label.encoding.text.field).toBe('Temp'); + expect(label.mark.baseline).toBe('middle'); + expect(label.encoding.color.condition?.test).toContain('Temp'); + }); +}); + +/** + * A wedge sits in no band, so the gap between two of them has to come out of + * the shapes themselves. The house says how wide, and says it separately from + * how it rules its bars — a half-pixel hairline that keeps two stacked + * segments apart disappears entirely on a circle. + */ +describe('holding wedges apart', () => { + const SHARE = [ + { Browser: 'Chrome', Share: 65 }, + { Browser: 'Safari', Share: 12 }, + { Browser: 'Edge', Share: 12 }, + { Browser: 'Firefox', Share: 6 }, + { Browser: 'Other', Share: 5 }, + ]; + const pie = (themeSpec: ThemeSpec, innerRadius = 0) => assembleVegaLite({ + data: { values: SHARE }, + semantic_types: { Browser: 'Category', Share: 'Percentage' }, + chart_spec: { + chartType: innerRadius ? 'Donut Chart' : 'Pie Chart', + encodings: { size: 'Share', color: 'Browser' }, + ...(innerRadius ? { chartProperties: { innerRadius } } : {}), + }, + theme_spec: themeSpec, + } as any) as any; + + const arcOf = (spec: any): any => { + let found: any; + JSON.stringify(spec, (_k, v) => { + if (!found && v && (v.type === 'arc' || v === 'arc')) found = v; + return v; + }); + return found; + }; + + it('cuts the wedges with a rule in the surface', () => { + const spec = pie(theme({ + marks: { slice: { gap: 1.5 } }, + } as Partial)); + const arc = arcOf(spec); + expect(arc.strokeWidth).toBe(1.5); + expect(arc.stroke).toBe('#ffffff'); + }); + + it('holds a donut apart the same way', () => { + const arc = arcOf(pie(theme({ marks: { slice: { gap: 2 } } } as Partial), 50)); + expect(arc.strokeWidth).toBe(2); + }); + + /** + * A house that says nothing about wedges is not silent: it has already + * said how it holds adjoining marks apart. + */ + it('falls back to the way the house rules its bars', () => { + const arc = arcOf(pie(theme({ + marks: { separator: { presence: 'hairline', source: 'surface', width: 1 } }, + } as Partial))); + expect(arc.strokeWidth).toBe(1); + }); + + it('leaves the wedges flush when the house holds nothing apart', () => { + const arc = arcOf(pie(theme({}))); + expect(arc.strokeWidth).toBeUndefined(); + }); + + it('swings the wedges apart when the house asks for an angle', () => { + const spec = pie(theme({ + marks: { slice: { gap: 4, gapStyle: 'pad' } }, + } as Partial)); + const arc = arcOf(spec); + expect(arc.padAngle).toBeGreaterThan(0); + expect(arc.stroke).toBeUndefined(); + // 5 wedges at 4px is well inside what the circumference can give. + expect(arc.padAngle).toBeLessThan((2 * Math.PI * 0.15) / 5); + }); + + /** + * Past a point the gaps are the chart. The house's width is honoured until + * the pads would take a sixth of the circle between them. + */ + it('holds the angle where the wedges would run out', () => { + const many = Array.from({ length: 40 }, (_, i) => ({ Browser: `B${i}`, Share: 1 })); + const spec = assembleVegaLite({ + data: { values: many }, + semantic_types: { Browser: 'Category', Share: 'Percentage' }, + chart_spec: { chartType: 'Pie Chart', encodings: { size: 'Share', color: 'Browser' } }, + theme_spec: theme({ marks: { slice: { gap: 8, gapStyle: 'pad' } } } as Partial), + } as any) as any; + const arc = arcOf(spec); + expect(arc.padAngle).toBeCloseTo((2 * Math.PI * 0.15) / 40, 6); + expect(spec._theme.report.some((r: any) => /held at/.test(r.message))).toBe(true); + }); +}); + +/** + * Two things a house says that nothing used to read: how much ink a connector + * deserves, and how far a label sits from the mark it names. + */ +describe('stems and the labels above them', () => { + const CO2 = [ + { Country: 'Qatar', Tonnes: 37 }, + { Country: 'UAE', Tonnes: 22 }, + { Country: 'India', Tonnes: 2 }, + ]; + const lollipop = (themeSpec: ThemeSpec) => assembleVegaLite({ + data: { values: CO2 }, + semantic_types: { Country: 'Country', Tonnes: 'Quantity' }, + chart_spec: { chartType: 'Lollipop Chart', encodings: { x: 'Country', y: 'Tonnes' } }, + theme_spec: themeSpec, + } as any) as any; + + const layerWith = (spec: any, type: string): any => { + let found: any; + walkSpec(spec, (n) => { + if (!found && n.mark && (n.mark.type ?? n.mark) === type) found = n; + }); + return found; + }; + function walkSpec(node: any, visit: (n: any) => void): void { + if (!node || typeof node !== 'object') return; + visit(node); + for (const key of ['layer', 'hconcat', 'vconcat', 'concat']) { + for (const child of node[key] ?? []) walkSpec(child, visit); + } + if (node.spec) walkSpec(node.spec, visit); + } + + /** + * The stem states a distance whose two ends are already drawn, so it is + * structure, not a series. + */ + it('demotes the stem to the house\'s structural hairline', () => { + const spec = lollipop(theme({ + marks: { connector: { presence: 'hairline', weight: 1 } }, + ink: { + surface: { canvas: '#ffffff' }, + series: { single: '#18a1cd' }, + structure: { rule: '#333333' }, + }, + } as Partial)); + const stem = layerWith(spec, 'rule'); + expect(stem.mark.strokeWidth).toBe(1); + expect(stem.mark.color).toBeTruthy(); + expect(stem.mark.color).not.toBe('#18a1cd'); + }); + + /** + * The offset is measured from the anchor, and a dot's anchor is its + * centre: the gap has to clear the radius before it is a gap at all. + */ + it('clears the dot before it starts counting the gap', () => { + const spec = lollipop(theme({ + dataLabels: { show: 'always', placement: 'outsideMark' }, + } as Partial)); + const label = layerWith(spec, 'text'); + const dot = layerWith(spec, 'circle') ?? layerWith(spec, 'point'); + const radius = Math.round(Math.sqrt(dot.mark.size / Math.PI)); + expect(radius).toBeGreaterThan(0); + expect(label.mark.dy).toBe(-(4 + radius)); + }); +}); + +describe('what a connector and a fit are allowed to say', () => { + /** + * A gridline is read through and sits at the bottom of the ordinal. A + * connector is read as part of the mark, so a house whose rules are + * already pale needs somewhere else to scale from. + */ + it('scales the connector from its own ink where the house states one', () => { + const spec = assembleVegaLite({ + data: { values: [{ Country: 'Qatar', Tonnes: 37 }, { Country: 'India', Tonnes: 2 }] }, + semantic_types: { Country: 'Country', Tonnes: 'Quantity' }, + chart_spec: { chartType: 'Lollipop Chart', encodings: { x: 'Country', y: 'Tonnes' } }, + theme_spec: theme({ + marks: { connector: { presence: 'full', weight: 1, style: 'dashed' } }, + ink: { + surface: { canvas: '#ffffff' }, + series: { single: '#18a1cd' }, + structure: { rule: '#dcdcdc', connector: '#c8c8c8' }, + }, + } as Partial), + } as any) as any; + let stem: any; + JSON.stringify(spec, (_k, v) => { + if (!stem && v?.type === 'rule') stem = v; + return v; + }); + expect(stem.color).toBe('#c8c8c8'); + expect(stem.strokeDash).toEqual([4, 3]); + }); + + /** + * A point on a line marks a reading. A fitted line has none: its vertices + * are where the fit was sampled. + */ + it('keeps the house\'s vertex points off a fitted line', () => { + const rows = Array.from({ length: 12 }, (_, i) => ({ HP: 40 + i * 15, MPG: 40 - i * 2 })); + const spec = assembleVegaLite({ + data: { values: rows }, + semantic_types: { HP: 'Quantity', MPG: 'Quantity' }, + chart_spec: { + chartType: 'Regression', + encodings: { x: 'HP', y: 'MPG' }, + chartProperties: { regressionMethod: 'linear' }, + }, + theme_spec: theme({ + marks: { point: { presence: 'full', size: 26 } }, + } as Partial), + } as any) as any; + const fit = (spec.layer ?? []).find( + (l: any) => (l.transform ?? []).some((t: any) => t.regression || t.loess), + ); + expect(fit).toBeTruthy(); + expect(fit.mark.point).toBe(false); + expect(spec.config.line.point).toBeTruthy(); + }); +}); + +/** + * Two dots and two connectors that a single number would get wrong. A vertex + * marker is found against a line the eye is already on; a scatter's dot is + * alone on the plot. A stem repeats a position already plotted; a bridge draws + * a distance plotted nowhere else. + */ +describe('what a connector joins, and what a dot has to carry alone', () => { + const PAIRS = [ + { Country: 'Japan', Sex: 'Male', Years: 81.5 }, { Country: 'Japan', Sex: 'Female', Years: 87.6 }, + { Country: 'Brazil', Sex: 'Male', Years: 69.0 }, { Country: 'Brazil', Sex: 'Female', Years: 76.0 }, + { Country: 'Nigeria', Sex: 'Male', Years: 51.0 }, { Country: 'Nigeria', Sex: 'Female', Years: 54.0 }, + ]; + + const marks = (spec: any): any[] => { + const out: any[] = []; + JSON.stringify(spec, (_k, v) => { + if (v && typeof v === 'object' && typeof v.type === 'string' && v.type !== 'quantitative') out.push(v); + return v; + }); + return out; + }; + + const dumbbell = (themeExtra: Partial): any => assembleVegaLite({ + data: { values: PAIRS }, + semantic_types: { Country: 'Category', Sex: 'Category', Years: 'Quantity' }, + chart_spec: { chartType: 'Ranged Dot Plot', encodings: { y: 'Country', x: 'Years', color: 'Sex' } }, + theme_spec: theme(themeExtra as any), + } as any) as any; + + it('draws the bridge at a mark\'s weight, not the stem\'s', () => { + const spec = dumbbell({ + marks: { + strokeWeight: 2, + connector: { presence: 'full', weight: 0.8, spanWeight: 3 }, + }, + ink: { surface: { canvas: '#ffffff' }, structure: { rule: '#d3dce1' } }, + } as Partial); + const bridge = marks(spec).find((m: any) => m.type === 'line'); + expect(bridge.strokeWidth).toBe(3); + // …and in structure's ink, not the ink of either dot it joins. + expect(bridge.color).toBe('#d3dce1'); + }); + + it('gives a silent house a bridge of its own line weight', () => { + const spec = dumbbell({ + marks: { strokeWeight: 1.6, connector: { presence: 'full', weight: 0.8 } }, + ink: { surface: { canvas: '#ffffff' }, structure: { rule: '#d3dce1' } }, + } as Partial); + expect(marks(spec).find((m: any) => m.type === 'line').strokeWidth).toBe(1.6); + }); + + it('sizes a dot the same wherever one is drawn', () => { + const rows = Array.from({ length: 12 }, (_, i) => ({ HP: 40 + i * 15, MPG: 40 - i * 2 })); + const spec = assembleVegaLite({ + data: { values: rows }, + semantic_types: { HP: 'Quantity', MPG: 'Quantity' }, + chart_spec: { chartType: 'Scatter Plot', encodings: { x: 'HP', y: 'MPG' } }, + theme_spec: theme({ + marks: { point: { presence: 'full', size: 45 } }, + } as Partial), + } as any) as any; + // The scatter is drawn as `circle`, which has its own config block — + // sizing `point` alone would have missed it entirely. + expect(spec.config.circle.size).toBe(45); + expect(spec.config.point.size).toBe(45); + expect(spec.config.line.point.size).toBe(45); + }); +}); + +/** + * A cell adjoins on both axes and its fill is the reading, so how far apart + * cells stand is not the same question as how far apart bars stand — and it is + * a real difference between houses: flush cells read as a continuous field, + * cut cells as a table of separate readings. + */ +describe('holding cells apart', () => { + const GRID = [ + { City: 'Cairo', Month: 'Jan', Temp: 14 }, { City: 'Cairo', Month: 'Feb', Temp: 15 }, + { City: 'Moscow', Month: 'Jan', Temp: -9 }, { City: 'Moscow', Month: 'Feb', Temp: -7 }, + ]; + + const heatmap = (themeExtra: Partial): any => assembleVegaLite({ + data: { values: GRID }, + semantic_types: { City: 'Category', Month: 'Category', Temp: 'Quantity' }, + chart_spec: { chartType: 'Heatmap', encodings: { x: 'Month', y: 'City', color: 'Temp' } }, + theme_spec: theme(themeExtra as any), + } as any) as any; + + const cell = (spec: any): any => { + let found: any; + JSON.stringify(spec, (_k, v) => { + if (!found && v?.type === 'rect') found = v; + return v; + }); + return found; + }; + + it('cuts the cells apart in the surface, not in an ink the scale never named', () => { + const spec = heatmap({ + marks: { tile: { gap: 1 } }, + ink: { surface: { canvas: '#ffffff' } }, + } as Partial); + expect(cell(spec).strokeWidth).toBe(1); + expect(cell(spec).stroke).toBe('#ffffff'); + }); + + it('leaves the field continuous where the house cuts nothing', () => { + const spec = heatmap({ ink: { surface: { canvas: '#ffffff' } } } as Partial); + // Untouched, the mark is still the bare string the template wrote. + expect(cell(spec)?.strokeWidth).toBeUndefined(); + }); + + it('holds cells apart the way it holds bars apart when it says nothing', () => { + const spec = heatmap({ + marks: { separator: { presence: 'hairline', source: 'surface', width: 1.5 } }, + ink: { surface: { canvas: '#ffffff' } }, + } as Partial); + expect(cell(spec).strokeWidth).toBe(1.5); + }); +}); diff --git a/packages/flint-js/tests/theme-titles.test.ts b/packages/flint-js/tests/theme-titles.test.ts new file mode 100644 index 00000000..5f1b44a7 --- /dev/null +++ b/packages/flint-js/tests/theme-titles.test.ts @@ -0,0 +1,109 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { describe, it, expect } from 'vitest'; +import { assembleVegaLite } from '../src'; +import type { ThemeSpec } from '../src/core/theme/types'; + +/** + * Who names the numbers. + * + * `Jan`, `Cairo`, `Chrome` say what kind of thing they are; `26`, `5300` do + * not, and something on the page has to. A theme can say where that naming + * goes — the axis, the headline — but it cannot say the naming is unnecessary, + * and the compiler holds it to that. + */ + +const MONTHLY = [ + { Month: 'Jan', Rainfall: 26 }, + { Month: 'Feb', Rainfall: 20 }, + { Month: 'Mar', Rainfall: 31 }, + { Month: 'Apr', Rainfall: 14 }, +]; + +function house(annotation: ThemeSpec['annotation'], legend?: ThemeSpec['legend']): ThemeSpec { + return { + id: 'house', + label: 'House', + ink: { surface: { canvas: '#ffffff' }, series: { single: '#333333' } }, + annotation, + ...(legend ? { legend } : {}), + } as ThemeSpec; +} + +function bars(themeSpec: ThemeSpec, headline?: string) { + return assembleVegaLite({ + data: { values: MONTHLY }, + semantic_types: { Month: 'Month', Rainfall: 'Amount' }, + chart_spec: { + chartType: 'Bar Chart', + ...(headline ? { title: headline } : {}), + encodings: { x: 'Month', y: 'Rainfall' }, + }, + theme_spec: themeSpec, + } as any) as any; +} + +/** `null` = suppressed, `undefined` = left to Vega-Lite (the field name). */ +function axisTitle(spec: any, channel: 'x' | 'y'): unknown { + const enc = (spec.spec?.encoding ?? spec.encoding ?? spec.layer?.[0]?.encoding ?? {})[channel]; + return enc?.axis?.title; +} + +describe('axis titles', () => { + it('omits a title over labels that name their own kind', () => { + const spec = bars(house({ axisTitles: 'whenAmbiguous' }), 'Wetter than it looks'); + expect(axisTitle(spec, 'x')).toBeNull(); + }); + + it('keeps a title over a column of bare values', () => { + const spec = bars(house({ axisTitles: 'whenAmbiguous' }), 'Wetter than it looks'); + expect(axisTitle(spec, 'y')).not.toBeNull(); + }); + + it('lets a house delegate the naming to its headline', () => { + const spec = bars(house({ axisTitles: 'omit' }), 'Wetter than it looks'); + expect(axisTitle(spec, 'y')).toBeNull(); + }); + + it('puts the title back when there is no headline to delegate to', () => { + const spec = bars(house({ axisTitles: 'omit' })); + expect(axisTitle(spec, 'y')).not.toBeNull(); + const report = spec._theme.report.map((r: any) => r.path); + expect(report).toContain('annotation.axisTitles'); + }); +}); + +describe('legend titles', () => { + const PENGUINS = [ + { Island: 'Biscoe', Species: 'Adelie', Mass: 3400 }, + { Island: 'Dream', Species: 'Gentoo', Mass: 5100 }, + { Island: 'Torgersen', Species: 'Adelie', Mass: 3700 }, + { Island: 'Biscoe', Species: 'Gentoo', Mass: 4900 }, + ]; + + function keyed(colorField: string) { + return assembleVegaLite({ + data: { values: PENGUINS }, + semantic_types: { Island: 'City', Species: 'Category', Mass: 'Amount' }, + chart_spec: { + chartType: 'Bar Chart', + title: 'Heavier in the west', + encodings: { x: 'Island', y: 'Mass', color: colorField }, + }, + theme_spec: house({ axisTitles: 'omit' }, { title: 'whenAmbiguous' }), + } as any) as any; + } + + it('leaves a key of names untitled', () => { + const spec = keyed('Species'); + expect(spec._theme.decisions.legend.show).toBe(true); + expect(spec._theme.decisions.legend.title).toBe(false); + }); + + it('titles a key of values', () => { + const spec = keyed('Mass'); + expect(spec._theme.decisions.legend.show).toBe(true); + expect(spec._theme.decisions.legend.title).toBe(true); + }); +}); diff --git a/packages/flint-mcp/assets/flint-chart-author.SKILL.md b/packages/flint-mcp/assets/flint-chart-author.SKILL.md index 52d1adc1..f8817e46 100644 --- a/packages/flint-mcp/assets/flint-chart-author.SKILL.md +++ b/packages/flint-mcp/assets/flint-chart-author.SKILL.md @@ -83,9 +83,11 @@ published, use the npm package or MCP server for released workflows. interface ChartAssemblyInput { // Bound by the HOST or by you, depending on the situation (see below). data: { values: any[] } | { url: string }; - semantic_types?: Record; // field → semantic type ← you write this + semantic_types?: Record; // field → type ( ← you write this) chart_spec: { // ← you write this chartType: string; // e.g. "Scatter Plot" + title?: string; // the headline — write one + subtitle?: string; // what is measured, of whom, when, in what units encodings: Record; // channel → { field, ... } (or array) baseSize?: { width: number; height: number }; // target layout size, default 400×320 canvasSize?: { width: number; height: number }; // optional hard ceiling on stretch @@ -93,6 +95,7 @@ interface ChartAssemblyInput { }; options?: Record; // global layout options (rarely needed) field_display_names?: Record; // field → readable axis/legend title + theme_spec?: string | ThemeSpec; // design language, e.g. "economist" (Vega-Lite only) } ``` @@ -168,6 +171,44 @@ For a Vega-Lite-specific style tweak: This edited Vega-Lite spec is no longer a portable Flint spec. Do not send it to `render_chart`; use `render_chart` only for Flint `ChartAssemblyInput`. +## Write a headline + +Set `chart_spec.title` to the finding, in a sentence, and `chart_spec.subtitle` +to the reading of it — what is measured, of whom, when, in what units: + +``` +title: "A pyramid that is no longer a pyramid" +subtitle: "United States population by age and sex, 2020, millions" +``` + +`Jan`, `Cairo`, `Chrome` name their own kind; `26`, `5,300`, `0.42` do not, and +the headline is where they get named. Leave it out only where the chart is not +read on its own — a sparkline in a cell, a tile under its own caption. Nothing +breaks: with no headline to lean on, the compiler keeps the axis titles instead. + +## Design languages (`theme_spec`) + +Name a house Flint ships and the compiler styles the chart to it: + +```json +{ "chart_spec": { ... }, "theme_spec": "economist" } +``` + +| id | what it is for | +| --- | --- | +| `nyt` | Newsroom graphics: headline states the finding, values on the marks, series named at their ends. | +| `economist` | Print weekly: compact, flat headline over a deck, units repeated down the ruler. | +| `nature` | Journal figure: small panel, axis titles with units, statistics beside the fit. | +| `mckinsey` | Consulting deck: wide bands, every value printed, headline states the takeaway. | +| `datawrapper` | Embedded web chart: narrow column, plain headline and deck, rule under the footer. | +| `powerbi` | Dashboard tile: compact, legend to the right, latest point emphasised. | + +A house governs the visual only — you still choose the fields, the aggregation +and the sort. Where a house depends on something only you can supply, it says +so: call `list_themes` with an `id` for that house's guidance, and read it +*before* writing the chart spec, since it may change how you prepare the data. +Vega-Lite only for now. You can also pass a `ThemeSpec` object of your own. + ## Step 1 — pick `chartType` Use one of the registered names **exactly**. Vega-Lite is the default and @@ -334,6 +375,29 @@ What choosing well gets you (automatically): If you don't know, use `Quantity` for numbers, `Category` for strings, `Date`/`DateTime` for date-shaped values. Do **not** invent type names. +### Saying more than the type name + +A field's entry can be an object instead of a string when the type alone +understates what you know: + +```json +"semantic_types": { + "anomaly": { "semanticType": "Quantity", "unit": "°C", "divergingMidpoint": 0 }, + "rating": { "semanticType": "Score", "intrinsicDomain": [1, 5] } +} +``` + +- `unit` — the unit or currency code: `"USD"`, `"°C"`, `"kg"`. +- `intrinsicDomain` — the field's own bounds, for bounded scales only: `[1, 5]` + for a five-star rating, `[0, 100]` for a percentage score. Not for + open-ended measures. +- `divergingMidpoint` — where the middle colour of a diverging scale sits. + Set it if you can tell what the reader is comparing against; leave it out if + you can't. +- `sortOrder` — the order the categories should appear in, when the order in + the data is not the one you want and it isn't alphabetical either: + `["Low", "Medium", "High"]`. For a handful of categories, not a long list. + ## Chart-level properties (`chartProperties`) `chartProperties` is an optional per-chart tuning map. Set a property only diff --git a/packages/flint-mcp/src/server.ts b/packages/flint-mcp/src/server.ts index 5a3de6ec..e474148a 100644 --- a/packages/flint-mcp/src/server.ts +++ b/packages/flint-mcp/src/server.ts @@ -14,7 +14,7 @@ import { renderChart, resolveDataSource } from './render/index.js'; import type { RenderBackend } from './render/types.js'; import { compileChart } from './tools/compile.js'; import { validateChart } from './tools/validate.js'; -import { listChartTypes } from './tools/list.js'; +import { listChartTypes, listThemes } from './tools/list.js'; import { buildAssemblyInputShape, toAssemblyInput, @@ -271,6 +271,28 @@ export function createServer(options: CreateServerOptions = {}): McpServer { }, ); + // --- list_themes -------------------------------------------------------- + server.registerTool( + 'list_themes', + { + title: 'List themes', + description: + 'List the design languages Flint ships, to be named in `theme_spec` ' + + '(e.g. theme_spec: "economist"). Pass an `id` to get that house\'s ' + + 'authoring guidance — what the spec must supply for it to work.', + inputSchema: { + id: z.string().optional(), + }, + }, + async (args: any) => { + try { + return jsonResult(listThemes(args?.id as string | undefined)); + } catch (err) { + return errorResult(err); + } + }, + ); + // --- create_chart_view (MCP App: interactive chart + config UI) --------- registerAppTool( server, diff --git a/packages/flint-mcp/src/tools/list.ts b/packages/flint-mcp/src/tools/list.ts index ddc86d8d..c9b56141 100644 --- a/packages/flint-mcp/src/tools/list.ts +++ b/packages/flint-mcp/src/tools/list.ts @@ -5,6 +5,8 @@ import { vlAllTemplateDefs, ecAllTemplateDefs, cjsAllTemplateDefs, + listThemePresets, + THEME_PRESETS, type ChartTemplateDef, } from 'flint-chart'; import type { RenderBackend } from '../render/types.js'; @@ -43,3 +45,22 @@ export function listChartTypes(backend?: RenderBackend): BackendCatalog[] { return { backend: b, count: chartTypes.length, chartTypes }; }); } + +/** + * The design languages Flint ships, to be named in `theme_spec`. + * + * Without an `id`, the catalogue: enough to choose by. With one, that house's + * guidance too — what an author has to supply for it to work, which is worth + * reading before writing the chart spec, not after. + */ +export function listThemes(id?: string) { + if (!id) return { themes: listThemePresets() }; + const preset = THEME_PRESETS[id]; + if (!preset) { + throw new Error( + `Unknown theme \`${id}\`. Flint ships: ${Object.keys(THEME_PRESETS).join(', ')}.`, + ); + } + const { spec: _spec, ...rest } = preset; + return rest; +} diff --git a/packages/flint-mcp/src/tools/schemas.ts b/packages/flint-mcp/src/tools/schemas.ts index de8176f1..c98794ac 100644 --- a/packages/flint-mcp/src/tools/schemas.ts +++ b/packages/flint-mcp/src/tools/schemas.ts @@ -54,6 +54,16 @@ export const chartSpecSchema = z chartType: z .string() .describe('Chart template name, e.g. "Bar Chart", "Scatter Plot", "Heatmap".'), + title: z + .string() + .optional() + .describe( + 'Headline: what this chart says, in words. Strongly recommended — many design languages drop axis titles and rely on the headline to name the measure, so a chart without one reads as unlabelled numbers.', + ), + subtitle: z + .string() + .optional() + .describe('Deck: what is measured, of whom, when, and in what units. E.g. "US population by age and sex, 2020, millions".'), encodings: z .record(z.string(), z.any()) .describe( @@ -88,8 +98,16 @@ export function buildAssemblyInputShape(disableFileReference = false) { semantic_types: z .record(z.string(), z.any()) .optional() - .describe('Field name → semantic type, e.g. { revenue: "Quantity", country: "Country" }.'), + .describe( + 'Field name → semantic type, e.g. { revenue: "Quantity", country: "Country" }. A value may instead be an annotation object when the type alone understates what you know, e.g. { rating: { semanticType: "Score", intrinsicDomain: [1, 5] } } — see the flint://agent-skill resource.', + ), chart_spec: chartSpecSchema, + theme_spec: z + .union([z.string(), z.record(z.string(), z.any())]) + .optional() + .describe( + 'Design language: the name of a house Flint ships (see list_themes, e.g. "economist") or a ThemeSpec object of your own. Vega-Lite only.', + ), options: z .record(z.string(), z.any()) .optional() @@ -110,11 +128,14 @@ export type AssemblyInputArgs = { chart_spec: { chartType: string; encodings: Record; + title?: string; + subtitle?: string; baseSize?: { width: number; height: number }; canvasSize?: { width: number; height: number }; chartProperties?: Record; }; options?: Record; + theme_spec?: string | Record; field_display_names?: Record; }; @@ -124,6 +145,7 @@ export function toAssemblyInput(args: AssemblyInputArgs): ChartAssemblyInput { data: args.data, semantic_types: args.semantic_types, chart_spec: args.chart_spec, + theme_spec: args.theme_spec, options: args.options, field_display_names: args.field_display_names, } as ChartAssemblyInput; diff --git a/packages/flint-mcp/tests/http.test.ts b/packages/flint-mcp/tests/http.test.ts index cc1b49b0..43d740ae 100644 --- a/packages/flint-mcp/tests/http.test.ts +++ b/packages/flint-mcp/tests/http.test.ts @@ -43,6 +43,7 @@ describe('MCP server over HTTP (stateless streamable transport)', () => { 'compile_chart', 'create_chart_view', 'list_chart_types', + 'list_themes', 'render_chart', 'validate_chart', ]); diff --git a/packages/flint-mcp/tests/server.test.ts b/packages/flint-mcp/tests/server.test.ts index bf3300a5..cd841bcb 100644 --- a/packages/flint-mcp/tests/server.test.ts +++ b/packages/flint-mcp/tests/server.test.ts @@ -52,6 +52,7 @@ describe('MCP server', () => { 'compile_chart', 'create_chart_view', 'list_chart_types', + 'list_themes', 'render_chart', 'validate_chart', ]); diff --git a/site/src/playground/ThemeLab.tsx b/site/src/playground/ThemeLab.tsx index 6909e4b4..627c6ff6 100644 --- a/site/src/playground/ThemeLab.tsx +++ b/site/src/playground/ThemeLab.tsx @@ -2,18 +2,21 @@ import { useEffect, useMemo, useState } from 'react'; import { VegaLiteView } from '../components/VegaLiteView'; import { ScaleToFit } from '../components/ScaleToFit'; import { siteTheme } from '../shared/theme'; +import { THEME_PRESETS, assembleVegaLite, vlGetTemplateDef } from 'flint-chart'; +import { PREVIEW_CASES } from './new-case-preview-data'; import THEME_META from './theme-lab-assets/_themes.json'; -import FLINT_INDEX from './theme-lab-assets/_flint-index.json'; +import HEADLINES from './theme-lab-assets/_headlines.json'; import THEME_SPECS from './theme-lab-assets/_themespecs.json'; /** * Theme lab — Flint's default Vega-Lite output next to a hand-authored - * bespoke redesign of the same chart, plus the diff between them. + * bespoke redesign of the same chart, plus the same chart compiled from the + * ThemeSpec the human read. * - * Nothing on this page is generated by a theming engine. Every "themed" spec - * in `theme-lab-assets/..json` was written by hand against the - * corresponding `.flint.json` baseline, so the third column is an honest - * inventory of what a design-theme language would actually have to control. + * Columns 1 and 3 are compiled here, in the browser, by the same compiler the + * library ships: a change to a preset or to a realize pass shows up on this + * page as soon as the module reloads. Only column 2 is a file — a human wrote + * it once, by hand, and it is the reference the other two are judged against. */ type ThemeId = keyof typeof THEME_META.themes; @@ -40,14 +43,21 @@ interface FlintIndexEntry { const THEMES = THEME_META.themes as unknown as Record; const THEME_ORDER = THEME_META.order as ThemeId[]; +const HEADLINE_MAP = HEADLINES.headlines as Record; -// Eagerly pull in every spec in the assets folder. `.flint.json` is the -// baseline; `..json` is the hand-authored redesign. -const SPEC_MODULES = import.meta.glob('./theme-lab-assets/*.json', { eager: true }) as Record< +// Column 2, and only column 2. `..json` is the hand-authored +// redesign; nothing else in this folder is read by the page. +const MANUAL_MODULES = import.meta.glob('./theme-lab-assets/*.json', { eager: true }) as Record< string, { default: any } >; +interface ThemeReport { + stage: 'ground' | 'realize'; + path: string; + message: string; +} + interface LabRow { id: string; chartType: string; @@ -57,42 +67,120 @@ interface LabRow { theme: ThemeId; flintSpec: any; themedSpec: any; + compiledSpec?: any; + compiledReport: ThemeReport[]; design: string[]; } -function buildRows(): LabRow[] { +/** + * Flint's `_`-prefixed bookkeeping (`_options`, `_width`, `_theme`) is not part + * of the Vega-Lite the reader is meant to see. A double underscore is + * load-bearing (`__geo_id` is a choropleth join key) and stays. + */ +function stripInternal(node: any): void { + if (!node || typeof node !== 'object') return; + if (Array.isArray(node)) { + node.forEach(stripInternal); + return; + } + for (const key of Object.keys(node)) { + if (/^_[^_]/.test(key)) delete node[key]; + else stripInternal(node[key]); + } +} + +/** + * The assembly input for a case, built the same way for both compiled columns — + * the only difference between them is whether a ThemeSpec is attached, which is + * the whole point of the comparison. + */ +function inputFor(c: (typeof PREVIEW_CASES)[number]): any { + const headline = HEADLINE_MAP[c.id] ?? { title: c.title }; + return { + data: { values: c.data }, + semantic_types: c.semantic_types, + chart_spec: { + chartType: c.chartType, + title: headline.title, + ...(headline.subtitle ? { subtitle: headline.subtitle } : {}), + encodings: c.encodings, + baseSize: { width: 340, height: 230 }, + ...(c.chartProperties ? { chartProperties: c.chartProperties } : {}), + }, + }; +} + +const CASE_INDEX: FlintIndexEntry[] = PREVIEW_CASES + .filter((c) => vlGetTemplateDef(c.chartType)) + .map((c) => { + const headline = HEADLINE_MAP[c.id] ?? { title: c.title }; + return { + id: c.id, + chartType: c.chartType, + title: headline.title, + subtitle: headline.subtitle ?? '', + source: c.source, + rows: c.data.length, + }; + }); + +function computeRows(): LabRow[] { // One row per (id, theme). A case may carry several hand-authored themes — // replicating one chart across every language is the only way to tell what // the language decided from what the chart forced. - const flintById = new Map(); - const themedById = new Map(); - for (const [path, mod] of Object.entries(SPEC_MODULES)) { + const manualById = new Map(); + for (const [path, mod] of Object.entries(MANUAL_MODULES)) { const file = path.split('/').pop()!; if (file.startsWith('_')) continue; - const match = /^(.*)\.([a-z-]+)\.json$/.exec(file); - if (!match) continue; - const [, id, kind] = match; - if (kind === 'flint') flintById.set(id, mod.default); - else themedById.set(id, [...(themedById.get(id) ?? []), mod.default]); + const match = /^(.*)\.([a-z0-9-]+)\.json$/.exec(file); + if (!match || !(match[2] in THEMES)) continue; + manualById.set(match[1], [...(manualById.get(match[1]) ?? []), mod.default]); } - const meta = new Map((FLINT_INDEX as FlintIndexEntry[]).map((e) => [e.id, e])); + const caseById = new Map(PREVIEW_CASES.map((c) => [c.id, c])); + const meta = new Map(CASE_INDEX.map((e) => [e.id, e])); const rows: LabRow[] = []; - for (const [id, themedList] of themedById) { - const flint = flintById.get(id); - if (!flint) continue; + + for (const [id, manualList] of manualById) { + const c = caseById.get(id); + if (!c || !vlGetTemplateDef(c.chartType)) continue; + const input = inputFor(c); const info = meta.get(id); - for (const themed of themedList) { + + let flint: any; + try { + flint = assembleVegaLite(input) as any; + stripInternal(flint); + } catch (err) { + console.error(`theme lab: ${id} failed to compile —`, (err as Error).message); + continue; + } + + for (const manual of manualList) { + const theme = manual.__theme__ as ThemeId; + const preset = (THEME_PRESETS as any)[theme]; + let compiled: any; + let report: ThemeReport[] = []; + try { + compiled = assembleVegaLite({ ...input, theme_spec: preset?.spec }) as any; + report = compiled._theme?.report ?? []; + stripInternal(compiled); + } catch (err) { + console.error(`theme lab: ${id}.${theme} failed to compile —`, (err as Error).message); + compiled = undefined; + } rows.push({ id, chartType: info?.chartType ?? '—', title: info?.title ?? id, source: info?.source ?? '', rows: info?.rows ?? 0, - theme: themed.__theme__ as ThemeId, + theme, flintSpec: flint, - themedSpec: themed, - design: (themed.__design__ as string[]) ?? [], + themedSpec: manual, + compiledSpec: compiled, + compiledReport: report, + design: (manual.__design__ as string[]) ?? [], }); } } @@ -102,6 +190,14 @@ function buildRows(): LabRow[] { ); } +// Compiling every case twice is cheap, but not cheap enough to do it once per +// call site. One pass per module load. +let ROWS_CACHE: LabRow[] | null = null; +function buildRows(): LabRow[] { + if (!ROWS_CACHE) ROWS_CACHE = computeRows(); + return ROWS_CACHE; +} + /** Vega-Lite ignores unknown top-level keys, but strip ours anyway. */ function cleanSpec(spec: any): any { const out: any = {}; @@ -194,14 +290,18 @@ function Thumb({ spec, bg, height }: { spec: any; bg: string; height: number }) } /** - * One wall tile = one (chart, language) pair, baseline left, redesign right, - * small enough to scan a whole language in one screen. The argument for the - * pair lives in the popup, not here. + * One wall tile = one (chart, language) triple: baseline, the human's redesign, + * and the compiler's reading of the same ThemeSpec. Small enough to scan a whole + * language in one screen; the argument lives in the popup, not here. */ function WallTile({ row, onOpen }: { row: LabRow; onOpen: () => void }) { const t = THEMES[row.theme]; const flint = useMemo(() => cleanSpec(row.flintSpec), [row.flintSpec]); const themed = useMemo(() => cleanSpec(row.themedSpec), [row.themedSpec]); + const compiled = useMemo( + () => (row.compiledSpec ? cleanSpec(row.compiledSpec) : null), + [row.compiledSpec], + ); return ( + ); + })} + + + {cases.map((c) => ( + + ))} +
+ ); +} diff --git a/site/src/playground/ThemeLabR2Cell.tsx b/site/src/playground/ThemeLabR2Cell.tsx new file mode 100644 index 00000000..67aba979 --- /dev/null +++ b/site/src/playground/ThemeLabR2Cell.tsx @@ -0,0 +1,125 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** + * A single (case × column) cell of the round-2 theme grid. It compiles its + * spec — Flint's default, or one house's ThemeSpec on top of it — the first + * time it scrolls into view, so a page of 80-odd cases across seven columns + * pays only for the panels a reader is actually looking at. + */ + +import { useEffect, useMemo, useRef, useState } from 'react'; +import { THEME_PRESETS, assembleVegaLite } from 'flint-chart'; +import { VegaLiteView } from '../components/VegaLiteView'; +import { siteTheme } from '../shared/theme'; +import { r2Input, type R2Case } from './theme-lab-r2-data'; + +export const R2_COLUMNS = ['flint', ...Object.keys(THEME_PRESETS)] as const; +export type R2Column = (typeof R2_COLUMNS)[number]; + +function stripInternal(node: any): void { + if (!node || typeof node !== 'object') return; + if (Array.isArray(node)) return node.forEach(stripInternal); + for (const key of Object.keys(node)) { + if (/^_[^_]/.test(key)) delete node[key]; + else stripInternal(node[key]); + } +} + +interface Compiled { + spec?: any; + background: string; + error?: string; + reportCount: number; +} + +function compileCell(c: R2Case, column: R2Column): Compiled { + try { + const input = r2Input(c); + const themeId = column === 'flint' ? null : column; + const spec = assembleVegaLite( + themeId ? { ...input, theme_spec: (THEME_PRESETS as any)[themeId].spec } : input, + ) as any; + const reportCount = spec._theme?.report?.length ?? 0; + const background = typeof spec.background === 'string' ? spec.background : '#ffffff'; + stripInternal(spec); + delete spec.$schema; + return { spec, background, reportCount }; + } catch (err) { + return { background: '#ffe8e8', error: (err as Error).message, reportCount: 0 }; + } +} + +export function R2Cell({ c, column }: { c: R2Case; column: R2Column }) { + const ref = useRef(null); + const [visible, setVisible] = useState(false); + + useEffect(() => { + const el = ref.current; + if (!el) return; + const observer = new IntersectionObserver( + ([entry]) => { + if (entry.isIntersecting) { + setVisible(true); + observer.disconnect(); + } + }, + { rootMargin: '300px' }, + ); + observer.observe(el); + return () => observer.disconnect(); + }, []); + + const built = useMemo(() => (visible ? compileCell(c, column) : null), [visible, c, column]); + + return ( +
+
+ {column} + {built && !built.error ? ( + {built.reportCount} notes + ) : null} +
+
+ {!built ? ( + + ) : built.error ? ( + + {built.error} + + ) : ( + + )} +
+
+ ); +} diff --git a/site/src/playground/theme-lab-gaps-data.ts b/site/src/playground/theme-lab-gaps-data.ts new file mode 100644 index 00000000..430bf504 --- /dev/null +++ b/site/src/playground/theme-lab-gaps-data.ts @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** + * Round-2 gaps: cases parked for inspection. + * + * The coverage round's rule (doc 05 §3) is that a fix must generalise: a change + * that only helps one chart is not a fix, it is a gap. Such cases are not left + * silently broken and they are not papered over with a per-chart hack — they + * are named here, with the reason they resist a general rule, so a human can + * look and decide whether the schema should grow to meet them. + * + * A gap references an R2 case by `id` (see `theme-lab-r2-data.ts`). `theme` + * names the column the problem shows in, or is omitted when it is the shape of + * the case itself, across houses, that is the gap. + */ + +export interface GapNote { + /** R2 case id the gap is about. */ + id: string; + /** The column it shows in, if it is house-specific. */ + theme?: string; + /** One line: what is wrong, and why it will not generalise. */ + note: string; +} + +export const GAP_NOTES: GapNote[] = [ + // Populated as the coverage round finds cases too particular to generalise + // from. Kept deliberately empty until then — a gap is a finding, not a + // placeholder. +]; diff --git a/site/src/playground/theme-lab-r2-data.ts b/site/src/playground/theme-lab-r2-data.ts new file mode 100644 index 00000000..2ab0ecff --- /dev/null +++ b/site/src/playground/theme-lab-r2-data.ts @@ -0,0 +1,187 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** + * Round-2 theme corpus. + * + * The theme lab's dev set has one case per chart type, all at a comfortable + * cardinality, all looked at while the compiler was being written. This file + * names a second corpus drawn from the gallery — charts written for entirely + * other purposes — chosen to vary the three things the lab holds constant: + * cardinality, data type, and chart shape. + * + * A case is a *reference* into `packages/flint-js/src/test-data`, not a copy of + * it: `(gen, index)` into `TEST_GENERATORS`. The gallery's own titles are + * shape descriptions ("N(5)×Q (5 pts)"), which tell a theme's typography + * nothing, so each case carries a plausible headline instead. + * + * `probe` says what the case is here to test. A case that stops probing + * anything should be removed, not kept for the count. + */ + +import { TEST_GENERATORS, type TestCase } from 'flint-chart/test-data'; +import { testCaseToAssemblyInput } from '../shared/test-case-utils'; + +export interface R2Case { + /** Stable slug: file name of the audit sheet, key of a gap note. */ + id: string; + /** `TEST_GENERATORS` key. */ + gen: string; + /** Index into that generator's cases. */ + index: number; + title: string; + subtitle?: string; + /** What this case is here to test. */ + probe: string; + family: R2Family; +} + +export type R2Family = + | 'Bars & ranking' + | 'Points & correlation' + | 'Distributions' + | 'Time & trends' + | 'Parts & radial' + | 'Maps & matrices' + | 'Single value & schedule'; + +export const R2_FAMILY_ORDER: R2Family[] = [ + 'Bars & ranking', + 'Points & correlation', + 'Distributions', + 'Time & trends', + 'Parts & radial', + 'Maps & matrices', + 'Single value & schedule', +]; + +export const R2_CASES: R2Case[] = [ + // ── Bars & ranking ──────────────────────────────────────────────────── + { id: 'bar-n5', gen: 'Bar Chart', index: 0, family: 'Bars & ranking', title: 'Revenue by segment', subtitle: 'Fiscal 2024, $m', probe: 'lowest useful bar count — a house that prints values has room for all of them' }, + { id: 'bar-n30', gen: 'Bar Chart', index: 2, family: 'Bars & ranking', title: 'Orders by product line', probe: '30 bands: label rotation, printed values, band occupancy' }, + { id: 'bar-n100', gen: 'Bar Chart', index: 3, family: 'Bars & ranking', title: 'Sessions by page', subtitle: 'Top 100 pages by traffic', probe: '100 bands, overflow and cutoff — every per-mark rule at its limit' }, + { id: 'bar-horizontal', gen: 'Bar Chart', index: 6, family: 'Bars & ranking', title: 'Downloads by platform', probe: 'horizontal bars: which axis is the index, which the measure' }, + { id: 'bar-temporal', gen: 'Bar Chart', index: 9, family: 'Bars & ranking', title: 'Monthly shipments', subtitle: 'Units, 2022–2023', probe: 'temporal band axis — tick policy on a bar chart' }, + { id: 'bar-temporal-color', gen: 'Bar Chart', index: 11, family: 'Bars & ranking', title: 'Shipments by region', subtitle: 'Units per month', probe: 'temporal bands with a nominal series on colour' }, + { id: 'bar-grid', gen: 'Bar Chart', index: 16, family: 'Bars & ranking', title: 'Category by tier', probe: 'two discrete positional channels on a bar template' }, + { id: 'stacked-n15', gen: 'Stacked Bar Chart', index: 1, family: 'Bars & ranking', title: 'Spend by department', subtitle: 'Five cost centres across fifteen teams', probe: '15 bands × 5 series: legend, printed values, stack order' }, + { id: 'stacked-numeric-color', gen: 'Stacked Bar Chart', index: 3, family: 'Bars & ranking', title: 'Responses by cohort', probe: 'numeric series values on colour — ordered, not nominal' }, + { id: 'stacked-horizontal-temporal', gen: 'Stacked Bar Chart', index: 9, family: 'Bars & ranking', title: 'Weekly hours by activity', probe: 'horizontal stack on a temporal index axis' }, + { id: 'grouped-numeric-color', gen: 'Grouped Bar Chart', index: 3, family: 'Bars & ranking', title: 'Scores by quarter', probe: 'grouped bars whose group field is numeric' }, + { id: 'grouped-sparse', gen: 'Grouped Bar Chart', index: 12, family: 'Bars & ranking', title: 'Sales by region and channel', subtitle: 'Not every channel operates in every region', probe: 'sparse cross-product: local dodge, lanes, legend' }, + { id: 'grouped-color-continuous', gen: 'Grouped Bar Chart', index: 8, family: 'Bars & ranking', title: 'Throughput by node', probe: '50 numeric groups — a ramp where a house expects a set' }, + { id: 'lollipop-color', gen: 'Lollipop Chart', index: 1, family: 'Bars & ranking', title: 'Adoption by market', probe: 'stem-and-dot marks with a colour series' }, + { id: 'lollipop-facet', gen: 'Lollipop Chart', index: 3, family: 'Bars & ranking', title: 'Adoption by market and tier', probe: 'lollipop inside small multiples' }, + { id: 'pyramid-12', gen: 'Pyramid Chart', index: 3, family: 'Bars & ranking', title: 'Households by income bracket', probe: '12 mirrored bands — two panels, two directions' }, + { id: 'pyramid-20', gen: 'Pyramid Chart', index: 6, family: 'Bars & ranking', title: 'Population by age band', subtitle: 'Twenty five-year bands', probe: 'pyramid past its comfortable height' }, + { id: 'bartable-diverging', gen: 'Bar Table', index: 4, family: 'Bars & ranking', title: 'Budget variance by team', probe: 'signed measure in a table — diverging ink, zero rule' }, + { id: 'bartable-long-labels', gen: 'Bar Table', index: 8, family: 'Bars & ranking', title: 'Programme spend', probe: 'category names wider than the gutter a theme leaves them' }, + { id: 'bartable-stacked', gen: 'Bar Table', index: 15, family: 'Bars & ranking', title: 'Views by month and channel', probe: 'a stacked bar inside a table row' }, + { id: 'bartable-facet-wrap', gen: 'Bar Table', index: 17, family: 'Bars & ranking', title: 'Leaders by market', probe: 'seven wrapped table panels' }, + { id: 'waterfall-typed', gen: 'Waterfall Chart', index: 1, family: 'Bars & ranking', title: 'Cash bridge', subtitle: 'Opening to closing balance, $m', probe: 'explicit step types — a three-role palette' }, + { id: 'waterfall-14', gen: 'Waterfall Chart', index: 3, family: 'Bars & ranking', title: 'Monthly cash flow', probe: '14 steps: connector, band width, printed values' }, + + // ── Points & correlation ────────────────────────────────────────────── + { id: 'scatter-color-n3', gen: 'Scatter Plot', index: 1, family: 'Points & correlation', title: 'Yield against temperature', subtitle: 'Three process lines', probe: 'the ordinary scatter: three series, one key' }, + { id: 'scatter-color-n50', gen: 'Scatter Plot', index: 12, family: 'Points & correlation', title: 'Latency against load', subtitle: 'By service', probe: '50 series — more categories than any house owns inks' }, + { id: 'scatter-shape', gen: 'Scatter Plot', index: 25, family: 'Points & correlation', title: 'Weight against economy', probe: 'series carried by shape, not colour' }, + { id: 'scatter-500', gen: 'Scatter Plot', index: 10, family: 'Points & correlation', title: 'Sensor readings', probe: '500 overlapping points — mark size and opacity' }, + { id: 'scatter-facet', gen: 'Scatter Plot', index: 28, family: 'Points & correlation', title: 'Height against weight', subtitle: 'By group', probe: 'faceted scatter' }, + { id: 'regression-6', gen: 'Regression', index: 3, family: 'Points & correlation', title: 'Cost against volume', subtitle: 'Six product families', probe: 'six fits at once — the statistics caption cannot speak' }, + { id: 'regression-quad', gen: 'Regression', index: 6, family: 'Points & correlation', title: 'Response against dose', probe: 'a non-linear fit' }, + { id: 'connected-3', gen: 'Connected Scatter Plot', index: 1, family: 'Points & correlation', title: 'Growth against inflation', subtitle: 'Three economies', probe: 'three trajectories, ordered by time' }, + { id: 'connected-spiral', gen: 'Connected Scatter Plot', index: 6, family: 'Points & correlation', title: 'Drift against noise', probe: 'a path that crosses itself 36 times' }, + { id: 'rangeddot', gen: 'Ranged Dot Plot', index: 0, family: 'Points & correlation', title: 'Before and after', subtitle: 'Eight sites', probe: 'two dots and a connector per row' }, + { id: 'strip-color', gen: 'Strip Plot', index: 1, family: 'Points & correlation', title: 'Trial scores by arm', probe: 'jittered sample with a colour series' }, + + // ── Distributions ───────────────────────────────────────────────────── + { id: 'hist-color', gen: 'Histogram', index: 1, family: 'Distributions', title: 'Age at diagnosis', subtitle: 'By sex', probe: 'two overlaid histograms' }, + { id: 'hist-1000', gen: 'Histogram', index: 2, family: 'Distributions', title: 'Part widths', probe: '1000 observations, many bins' }, + { id: 'density-3', gen: 'Density Plot', index: 1, family: 'Distributions', title: 'Latency by region', probe: 'three overlapping densities' }, + { id: 'density-facet', gen: 'Density Plot', index: 3, family: 'Distributions', title: 'Latency by region and site', probe: 'density inside small multiples' }, + { id: 'box-12', gen: 'Boxplot', index: 2, family: 'Distributions', title: 'Response time by service', probe: '12 boxes — band width against house preference' }, + { id: 'box-6x4', gen: 'Boxplot', index: 4, family: 'Distributions', title: 'Salary by department and level', probe: 'grouped boxes, six bands of four' }, + { id: 'box-sparse', gen: 'Boxplot', index: 7, family: 'Distributions', title: 'Salary by department and grade', subtitle: 'Not every grade exists in every department', probe: 'sparse box lanes' }, + { id: 'violin-zero', gen: 'Violin Plot', index: 3, family: 'Distributions', title: 'Daily returns by asset', probe: 'a distribution that crosses zero' }, + { id: 'violin-grid', gen: 'Violin Plot', index: 9, family: 'Distributions', title: 'Salary by department and level', probe: 'violins promoted to a grid' }, + { id: 'ecdf-2', gen: 'ECDF Plot', index: 1, family: 'Distributions', title: 'Test scores', subtitle: 'Control against treatment', probe: 'two cumulative curves' }, + { id: 'ecdf-facet', gen: 'ECDF Plot', index: 7, family: 'Distributions', title: 'Exam scores by subject', probe: 'faceted ECDF' }, + + // ── Time & trends ───────────────────────────────────────────────────── + { id: 'line-8', gen: 'Line Chart', index: 2, family: 'Time & trends', title: 'Traffic by channel', subtitle: 'Eight channels, daily', probe: 'eight series — more lines than a house names at the end' }, + { id: 'line-sparse', gen: 'Line Chart', index: 4, family: 'Time & trends', title: 'Coverage by cohort', probe: 'series with holes in them' }, + { id: 'line-continuous-color', gen: 'Line Chart', index: 5, family: 'Time & trends', title: 'Temperature over time', probe: 'a line whose colour carries a quantity' }, + { id: 'line-ordinal-30', gen: 'Line Chart', index: 8, family: 'Time & trends', title: 'Score by round', probe: '30 ordinal steps on the index axis' }, + { id: 'line-forecast', gen: 'Line Chart', index: 17, family: 'Time & trends', title: 'Demand, actual and forecast', subtitle: 'Three regions', probe: 'a dash that carries meaning, three times over' }, + { id: 'line-facet', gen: 'Line Chart', index: 20, family: 'Time & trends', title: 'Monthly revenue by region', probe: 'ten wrapped line panels' }, + { id: 'area-color-n4', gen: 'Area Chart', index: 1, family: 'Time & trends', title: 'Storage by tier', subtitle: 'Four tiers, monthly', probe: 'a four-band stack over time' }, + { id: 'area-ordinal', gen: 'Area Chart', index: 7, family: 'Time & trends', title: 'Visits by channel', probe: 'a stack over an ordinal index' }, + { id: 'area-facet', gen: 'Area Chart', index: 13, family: 'Time & trends', title: 'Monthly visits by channel', probe: 'faceted area' }, + { id: 'stream-5', gen: 'Streamgraph', index: 0, family: 'Time & trends', title: 'Attention by topic', probe: 'five wiggling bands and their names' }, + { id: 'rangearea-2', gen: 'Range Area Chart', index: 1, family: 'Time & trends', title: 'Monthly temperature range', subtitle: 'Two cities', probe: 'two bands over one index' }, + { id: 'rangearea-zero', gen: 'Range Area Chart', index: 4, family: 'Time & trends', title: 'Temperature anomaly band', probe: 'a band straddling zero' }, + { id: 'rangearea-facet', gen: 'Range Area Chart', index: 7, family: 'Time & trends', title: 'Daily temperature range by city', probe: 'faceted band' }, + { id: 'sparkline-3', gen: 'Sparkline', index: 1, family: 'Time & trends', title: 'Key metrics', probe: 'a table of trends — theme against a concatenation' }, + { id: 'sparkline-15', gen: 'Sparkline', index: 3, family: 'Time & trends', title: 'Metrics by service', probe: '15 rows of sparkline' }, + { id: 'bump-many', gen: 'Bump Chart', index: 2, family: 'Time & trends', title: 'Rank by quarter', subtitle: 'Twelve competitors', probe: 'twelve ranked series' }, + { id: 'bump-single', gen: 'Bump Chart', index: 4, family: 'Time & trends', title: 'Rank over four rounds', probe: 'a bump chart with one series and no key' }, + { id: 'slope-crossings', gen: 'Slope Chart', index: 1, family: 'Time & trends', title: 'Share, 2019 against 2024', subtitle: 'Eight companies', probe: 'eight slopes that cross' }, + { id: 'slope-negative', gen: 'Slope Chart', index: 3, family: 'Time & trends', title: 'Margin, before and after', probe: 'slopes across zero' }, + { id: 'slope-detail', gen: 'Slope Chart', index: 8, family: 'Time & trends', title: 'Units, before and after', probe: 'series carried by detail — no colour, no key' }, + { id: 'candle-90', gen: 'Candlestick Chart', index: 1, family: 'Time & trends', title: 'Daily price', subtitle: '90 sessions', probe: 'a dense composite mark' }, + { id: 'candle-facet', gen: 'Candlestick Chart', index: 3, family: 'Time & trends', title: 'Daily price by ticker', probe: 'faceted OHLC' }, + + // ── Parts & radial ──────────────────────────────────────────────────── + { id: 'pie-10', gen: 'Pie Chart', index: 1, family: 'Parts & radial', title: 'Share by vendor', probe: 'ten slices — more than a pie reads, which is the point' }, + { id: 'pie-25', gen: 'Pie Chart', index: 2, family: 'Parts & radial', title: 'Share by vendor', subtitle: 'Twenty-five vendors', probe: 'a pie past legibility' }, + { id: 'pie-skewed', gen: 'Pie Chart', index: 3, family: 'Parts & radial', title: 'Share by tier', probe: 'one slice at 80 per cent, five slivers' }, + { id: 'donut-4', gen: 'Donut Chart', index: 0, family: 'Parts & radial', title: 'Traffic by source', probe: 'the hole in the middle' }, + { id: 'rose-stacked', gen: 'Rose Chart', index: 1, family: 'Parts & radial', title: 'Wind by direction and season', probe: 'stacked petals' }, + { id: 'rose-months', gen: 'Rose Chart', index: 4, family: 'Parts & radial', title: 'Rainfall by month', probe: 'a cyclic ordinal index with an inner radius' }, + { id: 'rose-facet', gen: 'Rose Chart', index: 6, family: 'Parts & radial', title: 'Wind by direction and site', probe: 'faceted rose' }, + { id: 'radar-2', gen: 'Radar Chart', index: 1, family: 'Parts & radial', title: 'Team profile', subtitle: 'Two squads across six measures', probe: 'two overlaid polygons' }, + { id: 'radar-facet', gen: 'Radar Chart', index: 3, family: 'Parts & radial', title: 'Profile by region', probe: 'faceted radar' }, + { id: 'radar-12', gen: 'Radar Chart', index: 6, family: 'Parts & radial', title: 'Product profile', subtitle: 'Twelve measures', probe: 'twelve spokes and their names' }, + + // ── Maps & matrices ─────────────────────────────────────────────────── + { id: 'heat-ordinal', gen: 'Heatmap', index: 3, family: 'Maps & matrices', title: 'Activity by month and cohort', probe: 'an ordinal axis against a nominal one' }, + { id: 'heat-temporal', gen: 'Heatmap', index: 5, family: 'Maps & matrices', title: 'Activity by site and day', probe: '80 temporal columns' }, + { id: 'heat-wide', gen: 'Heatmap', index: 7, family: 'Maps & matrices', title: 'Coverage by test and suite', probe: 'a grid 80 wide and 5 tall' }, + { id: 'map-us', gen: 'Map', index: 0, family: 'Maps & matrices', title: 'US metro areas', probe: 'a projection: no axes, no bands' }, + { id: 'choropleth-us', gen: 'Choropleth', index: 0, family: 'Maps & matrices', title: 'Rate by state', probe: 'a ramp over geography' }, + + // ── Single value & schedule ─────────────────────────────────────────── + { id: 'gantt-project', gen: 'Gantt Chart', index: 0, family: 'Single value & schedule', title: 'Project schedule', probe: 'a temporal range per row' }, + { id: 'gantt-ci', gen: 'Gantt Chart', index: 1, family: 'Single value & schedule', title: 'Pipeline run', subtitle: 'Seconds from start', probe: 'a numeric range per row' }, + { id: 'bullet-12', gen: 'Bullet Chart', index: 1, family: 'Single value & schedule', title: 'Revenue against target', subtitle: 'Twelve stores', probe: 'twelve rows of measure, target and bands' }, +]; + +/** The base canvas every R2 case is compiled at, so sheets are comparable. */ +export const R2_BASE_SIZE = { width: 400, height: 280 }; + +const CASE_CACHE = new Map(); + +export function r2TestCase(c: R2Case): TestCase { + const key = `${c.gen}#${c.index}`; + const hit = CASE_CACHE.get(key); + if (hit) return hit; + const gen = TEST_GENERATORS[c.gen]; + if (!gen) throw new Error(`R2 case ${c.id}: no generator \`${c.gen}\``); + const cases = gen(); + const t = cases[c.index]; + if (!t) throw new Error(`R2 case ${c.id}: ${c.gen}[${c.index}] of ${cases.length}`); + CASE_CACHE.set(key, t); + return t; +} + +/** + * The assembly input for a case. Identical for all seven columns — the only + * difference between them is whether a ThemeSpec is attached. + */ +export function r2Input(c: R2Case): any { + const t = r2TestCase(c); + const input = testCaseToAssemblyInput(t, R2_BASE_SIZE); + input.chart_spec.title = c.title; + if (c.subtitle) input.chart_spec.subtitle = c.subtitle; + return input; +} From 8c731a94598b33bcab0283ca1093f72e9ae7f9f8 Mon Sep 17 00:00:00 2001 From: Chenglong Wang Date: Wed, 29 Jul 2026 21:59:23 -0700 Subject: [PATCH 010/164] theme(r2): gate always-labels on legibility; readable R2 tiles at 300/450 - ground.ts: dataLabels.show:always now yields to the same band+marks fit facts as whenTheyFit (shared metric, higher marks cap 120), so nyt/mckinsey stop smearing values on bar-n100, pie-25, grouped-color-continuous while keeping pie-10/pyramid-12 labelled. Fixes a first attempt that divided band by series and wrongly suppressed mirrored pyramids. - theme-lab-r2-data: design at 300x300 with a 450x450 stretch ceiling. - ThemeLabR2 page: fixed-width theme-lab tiles in a horizontal-scroll row (no more unreadable 7-col shrink), ScaleToFit charts, pill metadata. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 33e4b998-4b58-4d7e-9cf8-c107b10e45c2 --- packages/flint-js/src/core/theme/ground.ts | 21 ++++++++++ site/src/playground/ThemeLabR2.tsx | 31 +++++++++++--- site/src/playground/ThemeLabR2Cell.tsx | 49 +++++++++++++++++----- site/src/playground/theme-lab-r2-data.ts | 11 ++++- 4 files changed, 95 insertions(+), 17 deletions(-) diff --git a/packages/flint-js/src/core/theme/ground.ts b/packages/flint-js/src/core/theme/ground.ts index c8827ba9..db630fc4 100644 --- a/packages/flint-js/src/core/theme/ground.ts +++ b/packages/flint-js/src/core/theme/ground.ts @@ -861,6 +861,27 @@ export function groundTheme(themeIn: ThemeSpec, ctx: GroundingContext): DesignDe : 'no banded axis to key values to — one number per datum would be noise, not a label'); } + if (dl.show === 'always' && dlShow) { + // `always` is a preference to print, not a licence to overprint. It + // reads fit by the same two facts `whenTheyFit` does — a band wide + // enough to stand a number in, and few enough marks that the numbers do + // not pile up — but it holds to that preference further: it keeps + // printing past `whenTheyFit`'s comfort margin, onto the tight-but- + // legible charts the cautious houses leave to a legend, and yields only + // when the marks are genuinely too dense (a hundred-odd bars, a dozen- + // plus pie slices) for the numbers to be read. + const band = signals.hasBandedAxis + ? (bindings.categoricalChannel === 'y' ? ctx.layout.yStep : ctx.layout.xStep) + : Infinity; + const marksOnScreen = Math.max(1, signals.categoryCount || ctx.table.length) * Math.max(1, signals.seriesCount); + if (band < (valueLabel.fontSize ?? 10) + 4 || marksOnScreen > 120) { + dlShow = false; + say('dataLabels.show', band < (valueLabel.fontSize ?? 10) + 4 + ? `\`always\` overridden — a ${Math.round(band)}px band cannot hold a number` + : `\`always\` overridden — ${marksOnScreen} marks would pile the numbers past reading`); + } + } + if (dl.show === 'whenTheyFit') { const band = signals.hasBandedAxis ? (bindings.categoricalChannel === 'y' ? ctx.layout.yStep : ctx.layout.xStep) diff --git a/site/src/playground/ThemeLabR2.tsx b/site/src/playground/ThemeLabR2.tsx index d4f73586..81690bf5 100644 --- a/site/src/playground/ThemeLabR2.tsx +++ b/site/src/playground/ThemeLabR2.tsx @@ -14,7 +14,7 @@ * time, and each cell compiles only when it scrolls into view. */ -import { useState } from 'react'; +import { useState, type ReactNode } from 'react'; import { siteTheme } from '../shared/theme'; import { R2_CASES, @@ -28,6 +28,25 @@ function byFamily(family: R2Family): R2Case[] { return R2_CASES.filter((c) => c.family === family); } +function Pill({ children }: { children: ReactNode }) { + return ( + + {children} + + ); +} + function Row({ c }: { c: R2Case }) { return (
@@ -38,15 +57,17 @@ function Row({ c }: { c: R2Case }) { — {c.subtitle} ) : null}
-
- {c.gen} · probe: {c.probe} +
+ {c.gen} + probe: {c.probe}
{R2_COLUMNS.map((col) => ( diff --git a/site/src/playground/ThemeLabR2Cell.tsx b/site/src/playground/ThemeLabR2Cell.tsx index 67aba979..158deff3 100644 --- a/site/src/playground/ThemeLabR2Cell.tsx +++ b/site/src/playground/ThemeLabR2Cell.tsx @@ -11,12 +11,17 @@ import { useEffect, useMemo, useRef, useState } from 'react'; import { THEME_PRESETS, assembleVegaLite } from 'flint-chart'; import { VegaLiteView } from '../components/VegaLiteView'; +import { ScaleToFit } from '../components/ScaleToFit'; import { siteTheme } from '../shared/theme'; import { r2Input, type R2Case } from './theme-lab-r2-data'; export const R2_COLUMNS = ['flint', ...Object.keys(THEME_PRESETS)] as const; export type R2Column = (typeof R2_COLUMNS)[number]; +/** Tile geometry — fixed so a chart is never flex-shrunk below legibility. */ +export const R2_TILE_WIDTH = 320; +const R2_TILE_HEIGHT = 320; + function stripInternal(node: any): void { if (!node || typeof node !== 'object') return; if (Array.isArray(node)) return node.forEach(stripInternal); @@ -76,7 +81,8 @@ export function R2Cell({ c, column }: { c: R2Case; column: R2Column }) {
{!built ? ( - +
+ … +
) : built.error ? ( - +
{built.error} - +
) : ( - + + + )}
diff --git a/site/src/playground/theme-lab-r2-data.ts b/site/src/playground/theme-lab-r2-data.ts index 2ab0ecff..3ae7f16f 100644 --- a/site/src/playground/theme-lab-r2-data.ts +++ b/site/src/playground/theme-lab-r2-data.ts @@ -156,8 +156,11 @@ export const R2_CASES: R2Case[] = [ { id: 'bullet-12', gen: 'Bullet Chart', index: 1, family: 'Single value & schedule', title: 'Revenue against target', subtitle: 'Twelve stores', probe: 'twelve rows of measure, target and bands' }, ]; -/** The base canvas every R2 case is compiled at, so sheets are comparable. */ -export const R2_BASE_SIZE = { width: 400, height: 280 }; +/** The base canvas every R2 case is designed at, so sheets are comparable. */ +export const R2_BASE_SIZE = { width: 300, height: 300 }; + +/** The ceiling a case may stretch to when its data needs more room (1.5×). */ +export const R2_CANVAS_SIZE = { width: 450, height: 450 }; const CASE_CACHE = new Map(); @@ -181,6 +184,10 @@ export function r2TestCase(c: R2Case): TestCase { export function r2Input(c: R2Case): any { const t = r2TestCase(c); const input = testCaseToAssemblyInput(t, R2_BASE_SIZE); + // A design size of 300² with a 450² ceiling: the house lays out at the + // base and is allowed to stretch each dimension up to 1.5× when the data + // (many bands, a long legend) needs the room. + input.chart_spec.canvasSize = R2_CANVAS_SIZE; input.chart_spec.title = c.title; if (c.subtitle) input.chart_spec.subtitle = c.subtitle; return input; From 4d2baf6973dc3ac5bb8e8837939cdb2307518f32 Mon Sep 17 00:00:00 2001 From: Chenglong Wang Date: Wed, 29 Jul 2026 22:13:21 -0700 Subject: [PATCH 011/164] =?UTF-8?q?theme(r2):=20fix=20nyt=20dotted=20lines?= =?UTF-8?q?=20=E2=80=94=20withhold=20redundant=20dash=20under=20direct=20l?= =?UTF-8?q?abels,=20gate=20showPoints=20on=20density?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit line-8 under nyt rendered as dotted smears: a per-series strokeDash (dash as a redundant channel, fired because 8 series > 5 inks) collapsed to stipple on a dense daily path, and showPoints:true drew ~100 haloed points per series that ate the line. - ground.ts: within redundantEncoding:whenNeeded, withhold dash/shape when each series is named at its own mark (seriesEnd/inline legend) — the label already carries identity. - templates/line.ts: withhold the per-observation point overlay when per-series point spacing falls below ~8px; nyt's endpoint-emphasis dots then activate correctly. Keeps points on sparse lines (line-ordinal-30). Same 'preference yields to fit' principle as the always-label floor. Full corpus 0 failures; round-1 lab 121/0; 42 theme tests pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 33e4b998-4b58-4d7e-9cf8-c107b10e45c2 --- packages/flint-js/src/core/theme/ground.ts | 15 ++++++++- .../flint-js/src/vegalite/templates/line.ts | 33 +++++++++++++++++-- 2 files changed, 44 insertions(+), 4 deletions(-) diff --git a/packages/flint-js/src/core/theme/ground.ts b/packages/flint-js/src/core/theme/ground.ts index db630fc4..d4fd78a7 100644 --- a/packages/flint-js/src/core/theme/ground.ts +++ b/packages/flint-js/src/core/theme/ground.ts @@ -1115,7 +1115,8 @@ export function groundTheme(themeIn: ThemeSpec, ctx: GroundingContext): DesignDe : undefined, redundantChannels: marksSpec.redundantChannels ?? [], redundantEncoding: marksSpec.redundantEncoding ?? 'never', - redundant: groundRedundancy(marksSpec, series, signals, say), + redundant: groundRedundancy(marksSpec, series, signals, + legendShow && (placement === 'seriesEnd' || placement === 'inline'), say), }; // --- facets ------------------------------------------------------------- @@ -1422,6 +1423,7 @@ function groundRedundancy( marksSpec: NonNullable, series: ResolvedSeriesInk, signals: Signals, + directlyLabeled: boolean, say: (path: string, message: string) => void, ): { shape: boolean; dash: boolean } { const off = { shape: false, dash: false }; @@ -1439,6 +1441,17 @@ function groundRedundancy( '`whenNeeded` withheld — the house has a distinct ink for every series'); return off; } + // Even with more series than inks, a redundant channel earns its noise + // only if the reader needs it to tell the series apart. When each series + // is named at its own mark — a line labelled at its end, a band at its + // last reading — that identity is already carried, and a dash spread + // over a dense path degrades into texture rather than a distinguishing + // mark. The name does the work `whenNeeded` was reaching for. + if (directlyLabeled) { + say('marks.redundantEncoding', + '`whenNeeded` withheld — each series is named at its own mark, so nothing else need tell them apart'); + return off; + } } const unsupported = channels.filter((c) => c === 'texture' || c === 'lightness'); if (unsupported.length) { diff --git a/packages/flint-js/src/vegalite/templates/line.ts b/packages/flint-js/src/vegalite/templates/line.ts index 484d1a5a..c61a34ca 100644 --- a/packages/flint-js/src/vegalite/templates/line.ts +++ b/packages/flint-js/src/vegalite/templates/line.ts @@ -28,11 +28,38 @@ export function applyInterpolate(mark: any, config?: Record): any { return setMarkProp(mark, 'interpolate', config.interpolate); } -function applyShowPoints(mark: any, config?: Record): any { - if (!config?.showPoints) return mark; +function applyShowPoints(mark: any, ctx: InstantiateContext): any { + if (!ctx.chartProperties?.showPoints) return mark; + // Points on a line name where a value was measured. Past the density at + // which they touch, they stop being points and become a texture that buries + // the line under them — a house habit meeting a fact about fit. Yield the + // overlay when the readings pack tighter than a small dot can sit apart. + if (pointsTooDense(ctx)) return mark; return setMarkProp(mark, 'point', true); } +/** + * True when a line carries more readings per series than can be drawn as + * separate dots: the per-series point spacing falls below the width a small + * dot needs to read as one. Measured at the base width (the honest floor before + * any stretch), against the densest reasonable series estimate (rows ÷ series). + */ +function pointsTooDense(ctx: InstantiateContext): boolean { + const rows = ctx.table?.length ?? 0; + if (rows === 0) return false; + const seriesField = ctx.resolvedEncodings?.color?.field ?? ctx.resolvedEncodings?.detail?.field; + let series = 1; + if (seriesField) { + const seen = new Set(); + for (const r of ctx.table) seen.add(r[seriesField]); + series = Math.max(1, seen.size); + } + const pointsPerSeries = rows / series; + const width = ctx.canvasSize?.width ?? 300; + const spacing = width / Math.max(1, pointsPerSeries); + return spacing < 8; +} + function isContinuousColor(ctx: InstantiateContext): boolean { const color = ctx.resolvedEncodings.color; if (!color?.field) return false; @@ -101,7 +128,7 @@ export const lineChartDef: ChartTemplateDef = { } defaultBuildEncodings(spec, ctx.resolvedEncodings); spec.mark = applyInterpolate(spec.mark, ctx.chartProperties); - spec.mark = applyShowPoints(spec.mark, ctx.chartProperties); + spec.mark = applyShowPoints(spec.mark, ctx); }, properties: [interpolateConfigProperty, showPointsProperty], // No `transpose`: a line pins its domain to `x` (never a vertical line, for any From 9212ba036eef9237d2743d720b0440ef8244ba72 Mon Sep 17 00:00:00 2001 From: Chenglong Wang Date: Wed, 29 Jul 2026 22:16:29 -0700 Subject: [PATCH 012/164] theme(r2): park scatter-color-n50 and pie-25 in theme-lab-gaps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two cases too particular for a general coverage-round fix: - scatter-color-n50: 50 nominal series exceed any palette; colours cycle and a top legend blows the width out. A real fix (measured legend wrap/fallback + a more-series-than-inks policy) is larger than a patch and risky blind. - pie-25: label smear fixed in iter 2, but palette exhausts past ~24 slices — inherent to a pie past legibility. Gaps page switched to the same fixed-tile horizontal-scroll layout as R2 so the parked cases stay readable. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 33e4b998-4b58-4d7e-9cf8-c107b10e45c2 --- site/src/playground/ThemeLabGaps.tsx | 6 ++--- site/src/playground/theme-lab-gaps-data.ts | 28 +++++++++++++++++++--- 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/site/src/playground/ThemeLabGaps.tsx b/site/src/playground/ThemeLabGaps.tsx index d3150811..76a9a086 100644 --- a/site/src/playground/ThemeLabGaps.tsx +++ b/site/src/playground/ThemeLabGaps.tsx @@ -49,10 +49,10 @@ export function ThemeLabGaps() { {c ? (
{columns.map((col) => ( diff --git a/site/src/playground/theme-lab-gaps-data.ts b/site/src/playground/theme-lab-gaps-data.ts index 430bf504..bc1ae627 100644 --- a/site/src/playground/theme-lab-gaps-data.ts +++ b/site/src/playground/theme-lab-gaps-data.ts @@ -25,7 +25,29 @@ export interface GapNote { } export const GAP_NOTES: GapNote[] = [ - // Populated as the coverage round finds cases too particular to generalise - // from. Kept deliberately empty until then — a gap is a finding, not a - // placeholder. + { + id: 'scatter-color-n50', + note: + 'Fifty nominal series on colour. No house owns fifty distinct inks, so the ' + + 'colours cycle and the key stops being a key — the probe\'s own point. Two ' + + 'things go wrong and neither has a fix that stays general at this size: the ' + + 'palette overflows (colours repeat, so the legend cannot name a point by its ' + + 'ink), and the houses that prefer a top legend (nyt, economist) lay all fifty ' + + 'items in one row, blowing the width out and crushing the plot into a corner. ' + + 'A real fix is a legend that wraps or falls back by measured width, and a ' + + 'policy for "more series than inks" (suppress-and-note, or roll up to top-N + ' + + 'other) — both larger than a coverage-round patch, and easy to get wrong blind. ' + + 'Parked for a human to decide how far the schema should grow.', + }, + { + id: 'pie-25', + note: + 'Twenty-five slices — a pie past legibility by construction (its probe). The ' + + 'label smear is now fixed (Iteration 2 suppresses always-labels past the marks ' + + 'floor), but the palette still overflows: past ~24 categories the qualitative ' + + 'inks exhaust and the tail of slices falls back to grey. Inherent to a ' + + '25-slice pie; the honest answer is "do not draw this pie", which no theme ' + + 'rule can assert for the author. Parked as the reference case for palette ' + + 'exhaustion on part-to-whole.', + }, ]; From 4ade020a990a4a9ed95b9699cbc4f120d2e5496f Mon Sep 17 00:00:00 2001 From: Chenglong Wang Date: Wed, 29 Jul 2026 22:34:50 -0700 Subject: [PATCH 013/164] theme: a violin/density plot summarises a distribution, so it prints no value and needs no band-end label MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A violin and a density plot both draw a distribution as an area-mark silhouette. Grounding treated them as ordinary bands: isSummarised (the fact that spares a box plot its data labels) only knew the boxplot mark, so these looked labelable, and bandFamily offered them the seriesEnd placement whose anchor is the band's last reading — meaningful on a streamgraph, an arbitrary near-zero tail on a distribution. Under the direct-labelling houses this stamped a number on the shape (Junior 61660.08, Treatment B 0.00217) and piled the series names at the baseline. Extend isSummarised to key off the chart type (Violin Plot, Density Plot), not just the mark; that one fact drives both gates — the data-label value is withheld and the in-band placement falls through to the house legend. Streamgraphs are not summarised and keep their inline end labels. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 33e4b998-4b58-4d7e-9cf8-c107b10e45c2 --- packages/flint-js/src/core/theme/ground.ts | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/packages/flint-js/src/core/theme/ground.ts b/packages/flint-js/src/core/theme/ground.ts index d4fd78a7..2ddd60e4 100644 --- a/packages/flint-js/src/core/theme/ground.ts +++ b/packages/flint-js/src/core/theme/ground.ts @@ -274,6 +274,11 @@ function guardHolds(guard: ThemeGuard, s: Signals): boolean { const SERIES_CHANNELS = ['color', 'group', 'detail', 'series', 'shape', 'stroke']; const FACET_CHANNELS = ['column', 'row', 'facet']; +// Charts whose subject is the shape of a distribution, drawn from an area mark +// rather than a summary mark. They summarise like a box plot does, so they take +// the same label escape — a printed value names a quantity they were chosen not +// to reduce to. (A box plot itself is caught earlier by its `boxplot` mark.) +const DISTRIBUTION_SHAPE_CHARTS = new Set(['Violin Plot', 'Density Plot']); function distinctCount(table: any[], field: string | undefined): number { if (!field) return 0; @@ -452,7 +457,13 @@ function deriveSignals(ctx: GroundingContext, b: Bindings): Signals { isSigned, isTemporal, isFaceted: Boolean(b.facetChannel), - isSummarised: ctx.markTypes.some((m) => m === 'boxplot' || m === 'errorbar' || m === 'errorband'), + isSummarised: ctx.markTypes.some((m) => m === 'boxplot' || m === 'errorbar' || m === 'errorband') + // A violin or density plot draws a distribution as a *shape* built + // from an area mark — the same escape a box plot gets from its + // `boxplot` mark, these earn from what they are, not how they draw. + // Their subject is the silhouette; a single number stamped on it + // names a quantity the chart was chosen not to reduce to. + || DISTRIBUTION_SHAPE_CHARTS.has(ctx.chartType), canvasWidth: Math.round(ctx.layout.subplotWidth || ctx.canvasSize.width), }; } @@ -778,7 +789,12 @@ export function groundTheme(themeIn: ThemeSpec, ctx: GroundingContext): DesignDe // the band at its last reading, which is where a reader's eye already is // when they ask which band this is. const lineFamily = ctx.markTypes.some((m) => m === 'line' || m === 'trail'); - const bandFamily = ctx.markTypes.some((m) => m === 'area'); + // A violin or density plot is an area too, but its band has no meaningful + // last reading to hang a name on: the shape is a distribution, and its + // right edge is an arbitrary tail near zero, not an endpoint the eye rests + // at. Grounding withholds the in-band placement from it, and the name falls + // through to the ranked list — a legend — where the shapes stay legible. + const bandFamily = ctx.markTypes.some((m) => m === 'area') && !signals.isSummarised; const placementRealizable = (p: LegendPlacement): boolean => { if (p === 'seriesEnd' || p === 'inline') return (lineFamily || bandFamily) && !signals.isFaceted; return true; From 59d3af40af51248b130d78e4667a413d721066c1 Mon Sep 17 00:00:00 2001 From: Chenglong Wang Date: Wed, 29 Jul 2026 22:37:28 -0700 Subject: [PATCH 014/164] waterfall: pin x-axis title to the field so the synthetic __wf_lead column stops leaking into it The connector span binds a synthetic `__wf_lead` field on x2. With no explicit title, Vega-Lite folds that helper's name into the axis title, which rendered as "Month, __wf_lead". Pin the x-axis title to the real field so the internal column never surfaces in the chart. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 33e4b998-4b58-4d7e-9cf8-c107b10e45c2 --- packages/flint-js/src/vegalite/templates/waterfall.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/flint-js/src/vegalite/templates/waterfall.ts b/packages/flint-js/src/vegalite/templates/waterfall.ts index 292eb820..f2f1a321 100644 --- a/packages/flint-js/src/vegalite/templates/waterfall.ts +++ b/packages/flint-js/src/vegalite/templates/waterfall.ts @@ -119,7 +119,11 @@ export const waterfallChartDef: ChartTemplateDef = { field: xField, type: "ordinal" as const, sort: null, - axis: { labelAngle: -45 }, + // x2 binds the synthetic `__wf_lead` field for the connector span; + // without an explicit title Vega-Lite folds that helper's name into + // the axis title ("Month, __wf_lead"). Pin the title to the real + // field so the internal column never surfaces. + axis: { labelAngle: -45, title: xField }, }; // ── Preserve facet encodings ───────────────────────────────── From 9be350f850e96f355acd7c358a19d1b9f9b185fb Mon Sep 17 00:00:00 2001 From: Chenglong Wang Date: Wed, 29 Jul 2026 22:43:38 -0700 Subject: [PATCH 015/164] histogram: declare the binned x as a banded index axis so a right/top measure house does not flip the bins MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A histogram's binned x is an index axis — the reader keys counts off its intervals — but the template never declared it banded, so grounding classed the quantitative field as a measure. A house that seats its measure axis opposite (economist: y on the right) then flipped the bins to the *top* of the plot, where the tick labels collided with the title and no longer lined up with the bars. Add declareLayoutMode marking x banded, matching the bar family and grounding's own stated intent. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 33e4b998-4b58-4d7e-9cf8-c107b10e45c2 --- packages/flint-js/src/vegalite/templates/bar.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/flint-js/src/vegalite/templates/bar.ts b/packages/flint-js/src/vegalite/templates/bar.ts index 2a3a1288..19e47cf5 100644 --- a/packages/flint-js/src/vegalite/templates/bar.ts +++ b/packages/flint-js/src/vegalite/templates/bar.ts @@ -406,6 +406,12 @@ export const histogramDef: ChartTemplateDef = { }, channels: ["x", "color", "column", "row"], markCognitiveChannel: 'length', + // A binned x is an index axis, not a measure: the reader keys counts off + // its intervals, and its identity comes from banding even though the field + // is quantitative. Declaring it banded keeps the count off it and stops a + // house that seats its *measure* axis opposite (economist's right/top) from + // flipping the bins to the top of the plot. + declareLayoutMode: () => ({ axisFlags: { x: { banded: true } } }), instantiate: (spec, ctx) => { defaultBuildEncodings(spec, ctx.resolvedEncodings); // `binCount` is the maxbins cap; 0 (auto) leaves the template's `bin: true` From 7ab69aa7aacb52c7057309d03be7abd47aa3cfcf Mon Sep 17 00:00:00 2001 From: Chenglong Wang Date: Wed, 29 Jul 2026 22:50:39 -0700 Subject: [PATCH 016/164] theme: hoist the facet when a zero rule turns a faceted unit into a layer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit applyZeroRule converted a unit spec into a layer by hand to append the zero rule, moving the encoding — facet channel and all — into the inner layer. Vega-Lite drops a facet/row/column split left inside a layer's encoding, so any faceted chart whose measure crossed zero (a small- multiple range-area over cities with sub-zero lows) silently lost its faceting and drew every panel's band in one plot as a sawtooth. Route the conversion through appendLayer, which already promotes the split to a real facet operator wrapping the layer. Non-faceted and already-layered charts are unchanged; the zero rule now draws per panel. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 33e4b998-4b58-4d7e-9cf8-c107b10e45c2 --- packages/flint-js/src/vegalite/theme.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/flint-js/src/vegalite/theme.ts b/packages/flint-js/src/vegalite/theme.ts index 0a90a184..754f726c 100644 --- a/packages/flint-js/src/vegalite/theme.ts +++ b/packages/flint-js/src/vegalite/theme.ts @@ -551,12 +551,11 @@ function applyZeroRule(spec: any, d: DesignDecisions, table: any[], say: (p: str }, encoding: { [channel]: { datum: 0 } }, }; - if (Array.isArray(body.layer)) body.layer.push(rule); - else { - const own = { mark: body.mark, encoding: body.encoding }; - delete body.mark; - body.layer = [own, rule]; - } + // `appendLayer` turns a unit into a layer *and* promotes a + // `facet`/`row`/`column` split to a real operator wrapping the + // layer — Vega-Lite drops that split if it is left inside a layer's + // encoding, which silently un-facets the chart. + appendLayer(body, rule); if (!said) { say('structure.grid.zero', 'the measure changes sign inside the plot — zero is drawn as its own rule, not as one gridline among the rest'); said = true; From 8e55baf2db75a8b4cefc156ea9574502f87feea3 Mon Sep 17 00:00:00 2001 From: Chenglong Wang Date: Wed, 29 Jul 2026 22:53:42 -0700 Subject: [PATCH 017/164] theme-lab-gaps: park grouped-color-continuous (datawrapper ramp washes out fill marks on white) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A sequential ramp whose low end approaches the plot surface makes filled bars invisible — right for a heatmap (cell gaps outline it), wrong for bars. The generalizable principle (a fill mark needs a contrast floor against its surface) touches every sequential-coloured chart and is too risky to land blind mid-round, so it is documented as a reference gap. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 33e4b998-4b58-4d7e-9cf8-c107b10e45c2 --- site/src/playground/theme-lab-gaps-data.ts | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/site/src/playground/theme-lab-gaps-data.ts b/site/src/playground/theme-lab-gaps-data.ts index bc1ae627..df769ed4 100644 --- a/site/src/playground/theme-lab-gaps-data.ts +++ b/site/src/playground/theme-lab-gaps-data.ts @@ -50,4 +50,25 @@ export const GAP_NOTES: GapNote[] = [ + 'rule can assert for the author. Parked as the reference case for palette ' + 'exhaustion on part-to-whole.', }, + { + id: 'grouped-color-continuous', + theme: 'datawrapper', + note: + 'Fifty numeric groups on a *sequential* ramp, drawn as filled bars. ' + + 'datawrapper\'s ramp runs from near-white to blue — right for a heatmap, ' + + 'where cell gaps outline even the palest cell, but on bars against a white ' + + 'plot the low end of the ramp is the plot surface, so half the bars vanish ' + + 'and the panel reads as blank. The other houses survive only because their ' + + 'ramps start darker; flint\'s purple low-end stays visible too. The real ' + + 'principle underneath is generalisable — a *filled* mark needs a contrast ' + + 'floor against the surface it sits on, and a sequential ramp whose low end ' + + 'meets that surface must be lifted, or the marks given a thin stroke to ' + + 'outline them (as the heatmap\'s gaps already do). But that touches every ' + + 'sequential-coloured chart (heatmaps most of all, where the pale end is ' + + 'deliberate) and needs the fill-vs-bordered-cell distinction drawn carefully; ' + + 'too big and too risky to land blind mid-round. Parked as the reference case ' + + 'for "ramp low-end washes out a fill mark against its surface", alongside a ' + + 'note that continuous colour on fifty grouped bars is a poor encoding to ' + + 'begin with.', + }, ]; From f88288c8f93f2d6de7794ff794ce193165617b87 Mon Sep 17 00:00:00 2001 From: Chenglong Wang Date: Wed, 29 Jul 2026 23:16:54 -0700 Subject: [PATCH 018/164] mckinsey: add diverging ramp so signed measures keep their sign A budget-variance table runs +120 to -80. McKinsey declared only a sequential ramp, so ground.ts painted -80 the palest blue and +120 the darkest -- the reader sees 'less/more', never 'minus/plus'. Every other house declares a diverging ramp; McKinsey did not. A single-hue house cannot diverge, so this is the one place it reaches for a second hue: accent blue below zero, a restrained warm above, its light surface tint at the break. selection.signed set to diverging to match. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 33e4b998-4b58-4d7e-9cf8-c107b10e45c2 --- .../src/core/theme/presets/mckinsey.ts | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/packages/flint-js/src/core/theme/presets/mckinsey.ts b/packages/flint-js/src/core/theme/presets/mckinsey.ts index 31d7274a..6746a7da 100644 --- a/packages/flint-js/src/core/theme/presets/mckinsey.ts +++ b/packages/flint-js/src/core/theme/presets/mckinsey.ts @@ -56,9 +56,28 @@ export const mckinsey: ThemePreset = { // interpolates it. "consumption": "interpolate" }, + // A signed measure crosses zero, and a single-hue ramp cannot + // say which side of it a value sits on — dark reads as "more", + // not "positive". The house is otherwise all blue, so the one + // place it must reach for a second hue is here: cool blue below + // zero, a restrained warm above, the light surface tint at the + // break. Cool-below / warm-above matches the other houses. + "diverging": { + "stops": [ + "#2251ff", + "#9db8d2", + "#eef3f8", + "#d98f6a", + "#b4472e" + ], + "neutral": "#eef3f8", + "space": "lab", + "endpointsAgainstSurface": true, + "consumption": "interpolate" + }, "selection": { "partToWhole": "sequentialRamp", - "signed": "sequential" + "signed": "diverging" } }, "accent": "#2251ff" From f6b54c8401352340a775ae111643c4110e21b578 Mon Sep 17 00:00:00 2001 From: Chenglong Wang Date: Wed, 29 Jul 2026 23:17:08 -0700 Subject: [PATCH 019/164] theme: two realize placement fixes for faceted tables and dual keys 6a: a faceted *table* (facet operator whose panel is an hconcat) rendered blank when a house hung furniture. applyFurniture wraps the chart in an outer vconcat, turning facet>concat into concat>facet>concat; Vega-Lite rescopes the panel width signals out of reach and every column collapses. This is the failure radialResistsFurniture already documents for the faceted disc -- the guard was just too narrow. Added facetedTableResistsFurniture: a facet-of-concat refuses the wrapper and keeps the house rule off, as the radial facet already does. Plain faceted units (line-facet, area-facet) are untouched. 6c: on a line with two encodings (colour=series, strokeDash=actual/forecast), the direct-label houses name the series at each line end and hide the colour key, but the strokeDash key stayed top-right where the end labels now sit, piling into a smear. Added relocateSecondaryLegends: once series are named at the line end, any surviving non-colour key steps to the foot of the plot. Houses that keep a legend are untouched. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 33e4b998-4b58-4d7e-9cf8-c107b10e45c2 --- packages/flint-js/src/vegalite/theme.ts | 57 +++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/packages/flint-js/src/vegalite/theme.ts b/packages/flint-js/src/vegalite/theme.ts index 754f726c..d66cdabd 100644 --- a/packages/flint-js/src/vegalite/theme.ts +++ b/packages/flint-js/src/vegalite/theme.ts @@ -2561,9 +2561,42 @@ function applySeriesEndLabels( const longest = estimateLongestLabel(table, seriesField) + (merged ? 6 : 0); growPadding(spec, domainChannel === 'x' ? 'right' : 'top', longest * (t.fontSize ?? 10) * 0.55 + 8); } + // Naming the series at the line end frees the corner the colour key used to + // hold — but a *second* encoding (a forecast dash, a shape) still keeps its + // own key, and Vega-Lite parks it top-right by default, right where the end + // labels now are. The two collide into an unreadable pile. The end labels + // own that edge now, so the surviving key steps down to the foot of the + // plot, where it has the width to itself. + relocateSecondaryLegends(spec, 'bottom', say); say('legend.placement', '`seriesEnd` realized as a synthesized text layer at each series\' last point'); } +/** + * When the series are named at the line end, a second, non-colour encoding + * (`strokeDash` for actual/forecast, `shape`, `size`, `opacity`) still draws a + * key, and its default corner is the top-right the end labels have just taken. + * Move each such surviving key to the foot of the plot, clear of the labels. + * Colour/fill/stroke are the series itself and are already spoken for. + */ +function relocateSecondaryLegends(spec: any, orient: 'bottom' | 'top', say: (p: string, m: string) => void): void { + const channels = ['strokeDash', 'shape', 'size', 'opacity'] as const; + let moved = 0; + walk(spec, (node) => { + if (!node.encoding) return; + for (const ch of channels) { + const enc = node.encoding[ch]; + if (enc?.field && enc.legend !== null) { + enc.legend = { ...(typeof enc.legend === 'object' && enc.legend ? enc.legend : {}), orient }; + moved++; + } + } + }); + if (moved) { + say('legend.orient', + `a second key sat where the end labels now are — moved to the ${orient}, clear of them`); + } +} + /** * The same idea as a series-end label, for a chart made of bands. * @@ -3086,6 +3119,24 @@ function radialResistsFurniture(spec: any): boolean { return radiusArc || (arc && faceted); } +/** + * A faceted *table* — a `facet` operator whose panel is itself a concatenation + * (a bar-table's label / bar / value columns laid side by side) — meets the + * same wall as the faceted disc in `radialResistsFurniture`, one level deeper. + * Wrapping `facet > concat` in an outer furniture `vconcat` yields + * `concat > facet > concat`, and Vega-Lite rescopes the inner panels' width + * signals out of the concat's reach: every column collapses and the whole + * block renders empty. A plain faceted unit (line-facet, area-facet) has no + * such nested signal and rides the wrapper unharmed, so this stays narrow — + * only the facet-of-concat is refused. Like the disc, the table is already its + * own stack of blocks, with no single width for a rule to run across. + */ +function facetedTableResistsFurniture(spec: any): boolean { + if (!spec.facet) return false; + const panel = spec.spec; + return !!(panel && (panel.hconcat || panel.vconcat || panel.concat)); +} + function applyFurniture(spec: any, d: DesignDecisions, table: any[], say: (p: string, m: string) => void): boolean { if (!d.furniture.length) return false; if (spec.vconcat || spec.hconcat || spec.concat) { @@ -3098,6 +3149,12 @@ function applyFurniture(spec: any, d: DesignDecisions, table: any[], say: (p: st say('furniture', 'not drawn — a radial chart is already its own block, with no edge to close'); return false; } + // A faceted table (facet of concat) collapses when wrapped — see + // `facetedTableResistsFurniture`. It is already its own stack of blocks. + if (facetedTableResistsFurniture(spec)) { + say('furniture', 'not drawn — a faceted table is already its own stack of blocks, with no single edge to close'); + return false; + } const before: any[] = []; const after: any[] = []; From e368ec306b97e6ff7b33bb7d1d5ff04056a1014c Mon Sep 17 00:00:00 2001 From: zl190 Date: Thu, 30 Jul 2026 15:58:28 +0800 Subject: [PATCH 020/164] fix(vegalite): carry resolved axis titles into the waterfall layers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Waterfall template rebuilds its own encoding objects, and it read only `.field` off `ctx.resolvedEncodings` — discarding the titles the assembler had already resolved from `field_display_names`. The bar layer hardcoded the raw field name, and the shared x encoding carried no title at all. Vega-Lite derives an axis title from the field name of every untitled encoding on a shared scale and concatenates them, so the untitled x also picked up the internal connector column: the axis rendered as "week, __wf_lead". Carrying the resolved titles across fixes both symptoms at once. An explicit title on the primary channel settles the whole shared scale, so the internal columns need no treatment of their own — `title: null` on them would blank the axis instead, since an explicit null anywhere in a shared-scale title set wins. Verified by compiling with vl.compile and reading the Vega axis titles, not just the assembled spec. Swept every bundled gallery case: Waterfall was the only template leaking an internal column into an axis title. 652 tests passing. Co-Authored-By: Claude Opus 5 (1M context) --- .../src/vegalite/templates/waterfall.ts | 17 +- .../flint-js/tests/waterfall-titles.test.ts | 154 ++++++++++++++++++ 2 files changed, 170 insertions(+), 1 deletion(-) create mode 100644 packages/flint-js/tests/waterfall-titles.test.ts diff --git a/packages/flint-js/src/vegalite/templates/waterfall.ts b/packages/flint-js/src/vegalite/templates/waterfall.ts index 292eb820..6f2c7c02 100644 --- a/packages/flint-js/src/vegalite/templates/waterfall.ts +++ b/packages/flint-js/src/vegalite/templates/waterfall.ts @@ -31,6 +31,13 @@ export const waterfallChartDef: ChartTemplateDef = { const yField: string = y?.field || 'Amount'; const colorField: string | undefined = color?.field; + // The assembler has already resolved the axis titles (including any + // `field_display_names` override) onto the encodings; the layers below + // rebuild their own encoding objects, so carry those titles across + // rather than falling back to the raw field names. + const xTitle = x?.title ?? xField; + const yTitle = y?.title ?? yField; + if (!spec.encoding) spec.encoding = {}; if (column) spec.encoding.column = column; if (row) spec.encoding.row = row; @@ -115,11 +122,19 @@ export const waterfallChartDef: ChartTemplateDef = { spec.transform = transforms; // ── Shared x encoding ──────────────────────────────────────── + // Vega-Lite derives an axis title from the field name of every untitled + // encoding on a shared scale and concatenates them, so leaving this + // untitled surfaced the internal connector column in the rendered axis + // ("week, __wf_lead"). An explicit title settles the whole shared scale. + // It has to be an explicit title rather than `title: null` on the + // internal bindings: a null on a primary channel resolves the merged + // axis title to nothing, blanking the axis for every layer. const xEnc = { field: xField, type: "ordinal" as const, sort: null, axis: { labelAngle: -45 }, + title: xTitle, }; // ── Preserve facet encodings ───────────────────────────────── @@ -176,7 +191,7 @@ export const waterfallChartDef: ChartTemplateDef = { y: { field: "__wf_prev_sum", type: "quantitative", - title: yField, + title: yTitle, ...(yDomain ? { scale: { domain: yDomain } } : {}), }, y2: { field: "__wf_sum" }, diff --git a/packages/flint-js/tests/waterfall-titles.test.ts b/packages/flint-js/tests/waterfall-titles.test.ts new file mode 100644 index 00000000..6849d84f --- /dev/null +++ b/packages/flint-js/tests/waterfall-titles.test.ts @@ -0,0 +1,154 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { describe, it, expect } from 'vitest'; +import { assembleVegaLite } from '../src'; + +/** + * Waterfall Chart axis titles (Vega-Lite). + * + * A waterfall is built from window transforms, so several layers bind internal + * `__wf_*` columns to the same x/y scales as the user's own fields. Vega-Lite + * derives an axis title by concatenating the titles of every field on a shared + * scale, so any internal field left untitled surfaces in the rendered axis + * ("Week, __wf_lead"). These tests assert the two invariants that keep the axes + * readable: + * + * - the titles the assembler resolved (including `field_display_names`) are + * the ones that reach the axes, and + * - no internal `__wf_*` column can contribute to an axis title. + */ + +const DATA = [ + { week: '2026-06-01', wsu_change: 1200 }, + { week: '2026-06-08', wsu_change: -430 }, + { week: '2026-06-15', wsu_change: 880 }, + { week: '2026-06-22', wsu_change: -210 }, +]; + +function build( + fieldDisplayNames?: Record, + chartProperties?: Record, +) { + return assembleVegaLite({ + data: { values: DATA }, + semantic_types: { week: 'Date', wsu_change: 'Quantity' }, + chart_spec: { + chartType: 'Waterfall Chart', + encodings: { x: { field: 'week' }, y: { field: 'wsu_change' } }, + baseSize: { width: 500, height: 320 }, + ...(chartProperties ? { chartProperties } : {}), + }, + ...(fieldDisplayNames ? { field_display_names: fieldDisplayNames } : {}), + } as never) as any; +} + +/** The same chart split into small multiples, which nests the layered unit. */ +function buildFaceted() { + const rows = DATA.flatMap((r) => [ + { ...r, region: 'East' }, + { ...r, region: 'West' }, + ]); + return assembleVegaLite({ + data: { values: rows }, + semantic_types: { week: 'Date', wsu_change: 'Quantity', region: 'Category' }, + chart_spec: { + chartType: 'Waterfall Chart', + encodings: { + x: { field: 'week' }, + y: { field: 'wsu_change' }, + column: { field: 'region' }, + }, + baseSize: { width: 640, height: 320 }, + }, + field_display_names: { wsu_change: 'WSU weekly change', week: 'Week (Mon, JST)' }, + } as never) as any; +} + +/** + * Every encoding object in the spec. Faceted output nests the layered unit under + * `spec.spec`, so recurse rather than only reading the top level. + */ +function allEncodings(spec: any): Array<[string, any]> { + const out: Array<[string, any]> = []; + const visit = (node: any) => { + if (!node || typeof node !== 'object') return; + for (const [channel, def] of Object.entries(node.encoding ?? {})) { + if (def && typeof def === 'object') out.push([channel, def]); + } + for (const layer of node.layer ?? []) visit(layer); + visit(node.spec); + }; + visit(spec); + return out; +} + +describe('Waterfall Chart axis titles', () => { + it('applies field_display_names to the x and y axes', () => { + const spec = build({ wsu_change: 'WSU weekly change', week: 'Week (Mon, JST)' }); + + expect(spec.encoding.x.title).toBe('Week (Mon, JST)'); + + const bar = spec.layer.find((l: any) => (l.mark?.type ?? l.mark) === 'bar'); + expect(bar.encoding.y.title).toBe('WSU weekly change'); + }); + + it('falls back to the raw field names when no display names are given', () => { + const spec = build(); + + expect(spec.encoding.x.title).toBe('week'); + + const bar = spec.layer.find((l: any) => (l.mark?.type ?? l.mark) === 'bar'); + expect(bar.encoding.y.title).toBe('wsu_change'); + }); + + it('never lets an internal __wf_* column contribute to an axis title', () => { + const specs = [ + build(), + build({ week: 'Week (Mon, JST)' }), + // Value labels bind two more internal columns to the y scale. + build({ week: 'Week (Mon, JST)' }, { showTextLabels: true }), + // Faceted output nests the layered unit one level down. + buildFaceted(), + ]; + + for (const spec of specs) { + // Vega-Lite derives a title from the field name whenever a positional + // encoding leaves `title` undefined, and concatenates every derived title + // on a shared scale — but an explicit title on the scale's primary + // channel wins outright. So an untitled internal column is only safe on a + // scale whose primary channel carries an explicit title. + for (const scale of ['x', 'y'] as const) { + const onScale = allEncodings(spec).filter(([channel]) => channel === scale || channel === `${scale}2`); + + const untitledInternal = onScale.filter( + ([, def]) => typeof def.field === 'string' && def.field.startsWith('__wf_') && def.title === undefined, + ); + if (untitledInternal.length === 0) continue; + + const titled = onScale.filter(([channel, def]) => channel === scale && typeof def.title === 'string'); + + expect( + titled.length, + `${scale} carries internal columns ${untitledInternal + .map(([, d]) => d.field) + .join(', ')} but no explicit title to stop Vega-Lite naming the axis after them`, + ).toBeGreaterThan(0); + } + } + }); + + it('never suppresses a shared axis title with a null on a primary channel', () => { + // Vega-Lite merges the titles of every layer on a shared scale, and an + // explicit null anywhere in that set wins — so a `title: null` used to hide + // an internal column on x or y blanks the axis for the whole chart. Only + // secondary channels (x2/y2) may opt out that way. + for (const spec of [build(), build({ week: 'W' }, { showTextLabels: true })]) { + const nulled = allEncodings(spec) + .filter(([channel, def]) => /^(x|y)$/.test(channel) && def.title === null) + .map(([channel, def]) => `${channel}:${def.field}`); + + expect(nulled).toEqual([]); + } + }); +}); From 9207c69ed457385a0f57627f2109b183742dfc16 Mon Sep 17 00:00:00 2001 From: zl190 Date: Thu, 30 Jul 2026 16:31:59 +0800 Subject: [PATCH 021/164] fix(flint-py): point run_full_eval at the moved fixture corpus `tools/run_full_eval.py` exits with "manifest.json not found" and evaluates nothing. `FIXTURES` still resolves to `packages/flint-py/tests/fixtures/`, but the corpus moved to `shared/test-data/` in 1d1ee39 and this constant did not follow. The report path is derived from `FIXTURES.parent`, so repointing the corpus alone would have carried the report out of `tests/` along with it. Giving the report its own `REPORT_DIR` keeps both outputs on the locations where the committed copies already live: `results.json` next to the corpus, `FULL_GALLERY_REPORT.md` under `tests/`. With this applied the tool completes and reports PASS 476 / MISMATCH 180 / PY_ERROR 0 / JS_ERROR 1 / FIXTURE_MISSING 0. The mismatch count agrees with `pytest -q`. Co-Authored-By: Claude Opus 5 (1M context) --- packages/flint-py/tools/run_full_eval.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/flint-py/tools/run_full_eval.py b/packages/flint-py/tools/run_full_eval.py index cf363202..f75d21cb 100644 --- a/packages/flint-py/tools/run_full_eval.py +++ b/packages/flint-py/tools/run_full_eval.py @@ -1,8 +1,8 @@ """Run the Python `assemble_vegalite` against every extracted JS fixture. Categorises each fixture as PASS, MISMATCH, PY_ERROR, or NO_EXPECTED, then -writes a structured `results.json` and a human-readable `REPORT.md` to -`flint-py/tests/fixtures/` for downstream analysis. +writes a structured `results.json` next to the corpus in `shared/test-data/` +and a human-readable `FULL_GALLERY_REPORT.md` to `flint-py/tests/`. Usage: python flint-py/tools/run_full_eval.py @@ -20,7 +20,11 @@ from typing import Any ROOT = Path(__file__).resolve().parent.parent -FIXTURES = ROOT / "tests" / "fixtures" +# The fixture corpus moved to shared/test-data in 1d1ee39 ("rename +# agents→agent-skills, test-fixtures→test-data"); results.json is tracked +# alongside it. The report stays under tests/, where the committed copy lives. +FIXTURES = ROOT.parent.parent / "shared" / "test-data" +REPORT_DIR = ROOT / "tests" sys.path.insert(0, str(ROOT)) from flint.vegalite import assemble_vegalite # noqa: E402 @@ -342,7 +346,7 @@ def main() -> int: "Without these fixes the report would show ~70 MISMATCH cases.\n" ) - out_path = FIXTURES.parent / "FULL_GALLERY_REPORT.md" + out_path = REPORT_DIR / "FULL_GALLERY_REPORT.md" out_path.write_text("".join(lines)) print(f"Wrote {out_path}") print("Summary:") From f168ad90d15bb9e835d6758e4cfc63158dfddde9 Mon Sep 17 00:00:00 2001 From: Chenglong Wang Date: Thu, 30 Jul 2026 08:00:02 -0700 Subject: [PATCH 022/164] theme: clear printed bar labels with scale headroom; wrap wide legends MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two label-overflow fixes found by inspecting rendered R2 audit sheets. bar-n30 / bar-n5 — a house that prints values above its bars clipped the labels on the tallest bars (TV 825, Speaker 749, HDD 707): the bar that defines the scale maximum reaches the plot ceiling and leaves no strip of plot above it for its label. Outer top padding cannot clear it because the title already sits there. Give the measure scale headroom instead, so the tallest bar stops short of the ceiling and every label lands in whitespace inside the plot — the way a labelled bar chart is actually drawn. With headroom the scale-end inside-flip is redundant, so it is dropped on vertical bars (kept on horizontal, where the margin, not the domain, is the remedy). scatter-color-n50 — nyt and economist lay a colour key horizontally at the top, and fifty names in one row run off the side of the canvas, crushing the plot to a sliver. A row has a width, so the entries are wrapped into as many columns as the block holds and capped to a few rows — the same overflow the baseline already resolves, restored to the themed path. All 85 R2 sheets render (0 failures); theme-lab unchanged bar the known exam-ecdf headline note; 694 tests pass; tsc clean. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 33e4b998-4b58-4d7e-9cf8-c107b10e45c2 --- packages/flint-js/src/vegalite/theme.ts | 121 +++++++++++++++++++++++- 1 file changed, 118 insertions(+), 3 deletions(-) diff --git a/packages/flint-js/src/vegalite/theme.ts b/packages/flint-js/src/vegalite/theme.ts index d66cdabd..6f4a89f2 100644 --- a/packages/flint-js/src/vegalite/theme.ts +++ b/packages/flint-js/src/vegalite/theme.ts @@ -1846,9 +1846,61 @@ function applyLegend(spec: any, config: any, d: DesignDecisions, table: any[], s `${widths.length} keys want ${Math.round(total)}px across a ${block}px block — they take a row each`); } } + + // A single key with many entries laid horizontally is the same overrun seen + // one key at a time: the house asked for one row, and one row of fifty names + // runs off the side of the canvas. A row has a width — the block the chart + // occupies — so the entries are wrapped into as many columns as fit and no + // more, and where even a wrapped grid would tower over the plot the count + // is capped. Vega-Lite draws a top or bottom legend in a single row unless + // told how many columns to fill, so it is told. + if (l.orient === 'top' || l.orient === 'bottom') { + wrapWideKeys(spec, l, blockWidth(spec, table), table, say); + } void say; } +/** + * Wrap a high-cardinality horizontal key into a bounded grid. + * + * A top or bottom legend flows its entries along a single row until told + * otherwise. One key with more names than fit across the block overruns the + * canvas, so the row is broken into as many columns as the block holds; and + * because a very long list would then grow downward without end, the number + * of entries drawn is capped to a few rows' worth. + */ +function wrapWideKeys( + spec: any, l: DesignDecisions['legend'], block: number | undefined, table: any[], + say: (p: string, m: string) => void, +): void { + if (!block) return; + const MAX_ROWS = 4; + walk(spec, (node) => { + for (const channel of ['color', 'fill', 'stroke'] as const) { + const enc = node.encoding?.[channel]; + if (!enc?.field || enc.legend === null || enc.type === 'quantitative') continue; + const entries: string[] = Array.isArray(enc.legend?.values) + ? enc.legend.values.map(String) + : Array.isArray(enc.scale?.domain) + ? enc.scale.domain.map(String) + : orderedValues(table, enc.field).map(String); + if (entries.length < 2) continue; + const labelFS = enc.legend?.labelFontSize ?? l.label.fontSize ?? 10; + const symbolArea = enc.legend?.symbolSize; + const symbol = symbolArea ? 2 * Math.sqrt(symbolArea / Math.PI) : 10; + const entryWidth = entries.reduce( + (m, e) => Math.max(m, symbol + 4 + e.length * labelFS * 0.55 + 10), 0); + const columns = Math.max(1, Math.floor(block / entryWidth)); + if (entries.length <= columns) continue; + enc.legend = { ...(enc.legend ?? {}), columns }; + const cap = columns * MAX_ROWS; + if (entries.length > cap) enc.legend.symbolLimit = cap; + say('legend.columns', + `${entries.length} keys in one row overrun the ${Math.round(block)}px block — wrapped to ${columns} columns`); + } + }); +} + /** * Roughly how wide each key drawn on this chart wants to be. * @@ -2299,17 +2351,36 @@ function labelOneBody(spec: any, body: any, d: DesignDecisions, table: any[], sa say('dataLabels.placement', message); }; + // A vertical bar's outside label is cleared by giving the measure scale + // headroom (below); a horizontal one by reserving right margin. The + // scale-end flip — printing the tallest bars' labels inside instead — + // solves the same "no room past the end" problem, so it is only needed + // where headroom is not the remedy: on horizontal bars. + const headroomClears = !inside && onMarkBody && !horizontal && !radial && !cells; + if (!radial && !cells && onMarkBody) { if (inside && d.dataLabels.insideMinValue != null) { split(d.dataLabels.insideMinValue, '<', 'marks shorter than their own label print it outside instead'); growPadding(spec, horizontal ? 'right' : 'top', (t.fontSize ?? 10) * 2); - } else if (!inside && d.dataLabels.outsideMaxValue != null) { + } else if (!inside && d.dataLabels.outsideMaxValue != null && !headroomClears) { split(d.dataLabels.outsideMaxValue, '>', 'marks that reach the end of the scale print their label inside instead'); } } - // Outside labels sit past the end of the mark, so the plot needs room. - if (!inside && horizontal) growPadding(spec, reversed ? 'left' : 'right', (t.fontSize ?? 10) * 3); + // Outside labels sit past the end of the mark, so the plot needs room — + // to the side the bar grows toward. A horizontal bar's label sits past its + // right end, in clean margin the padding reserves. A vertical bar's sits + // above its top, where the title already is, so reserving outer padding + // does not clear it; instead the measure scale is given headroom so the + // tallest bar stops short of the plot ceiling and its label lands in + // whitespace inside the plot. + if (!inside && !radial && !cells) { + if (horizontal) { + growPadding(spec, reversed ? 'left' : 'right', (t.fontSize ?? 10) * 3); + } else if (onMarkBody) { + addMeasureHeadroom(body, measureChannel, measure.field, table); + } + } return layer; } @@ -2370,6 +2441,50 @@ function growPadding(spec: any, side: 'left' | 'right' | 'top' | 'bottom', amoun spec.padding = base; } +/** + * A value label printed above a bar needs a strip of plot above the bar to sit + * in. The bar that defines the scale's maximum reaches the top of the plot and + * leaves none, so the scale is given a margin: the domain is pushed past the + * data by a fraction of its span, on whichever end carries labelled bar ends. + * A stack of bars rising from zero keeps its zero; only the outward end moves. + */ +function addMeasureHeadroom( + body: any, channel: 'x' | 'y' | 'theta' | 'color', field: string, table: any[], +): void { + const HEADROOM = 0.15; + const nums = table.map((r) => r?.[field]).filter((v: any) => typeof v === 'number' && Number.isFinite(v)) as number[]; + if (nums.length === 0) return; + const dataMax = Math.max(...nums); + const dataMin = Math.min(...nums); + // Bars stand on zero, so the span the labels have to clear is measured from + // zero, not from the smallest bar. + const span = Math.max(dataMax, 0) - Math.min(dataMin, 0); + if (!(span > 0)) return; + const pad = span * HEADROOM; + // `appendLayer` has already run: the mark that carried the measure is no + // longer `body` but a layer inside it (and may have been pushed under a + // hoisted facet operator). Follow the same path to the node that now owns + // the encodings, and set the scale on every place the measure appears — + // the shared encoding and each layer that repeats the field — so the + // shared scale Vega-Lite resolves across the layers picks the wider domain. + const host = body.spec?.layer ? body.spec : body; + const encodings: any[] = []; + if (host.encoding?.[channel]?.field === field) encodings.push(host.encoding[channel]); + for (const u of (host.layer ?? [])) { + if (u?.encoding?.[channel]?.field === field) encodings.push(u.encoding[channel]); + } + for (const encChannel of encodings) { + const scale = { ...(encChannel.scale ?? {}) }; + // An explicit domain is a deliberate choice; headroom does not override it. + if (scale.domain != null) continue; + // The label sits past the end of the bar — above a positive bar, below + // a negative one — so each populated end is given room. + if (dataMax > 0 && scale.domainMax == null) scale.domainMax = dataMax + pad; + if (dataMin < 0 && scale.domainMin == null) scale.domainMin = dataMin - pad; + encChannel.scale = scale; + } +} + // --------------------------------------------------------------------------- // seriesEnd — the second thing Vega-Lite has no primitive for // --------------------------------------------------------------------------- From 0e306b8efe00c17d74cba47ed832551995627905 Mon Sep 17 00:00:00 2001 From: Chenglong Wang Date: Thu, 30 Jul 2026 10:13:03 -0700 Subject: [PATCH 023/164] theme: fit-gate bar value labels, pie small-slice suppression, flush-left legends Bar value labels: measure the value-label width against the per-bar slot (band step, or band/seriesCount for grouped/dodged) and, when the label cannot fit, flip a single-series bar's label outside the mark and suppress it for grouped bars (narrow sub-bands with no borrowable padding). Keeps the legend/axis intact. readableOn: pick whichever candidate ink sits furthest from the background in luminance instead of assuming which one is light. Fixes invisible contrast labels on dark houses (powerbi) while leaving light houses unchanged; contrast is a legibility floor, not a fact about which ink a house names "primary". Pie: hide value labels on slices whose arc at the label radius is thinner than one line, via conditional opacity (not a row filter, which would recompute the shared stacked theta and drift the remaining labels off their arcs). Legend: lay a horizontal top/bottom key flush-left with the start-anchored title (anchor:'start', bounds:'full') so the caption begins where the graphic begins, rather than indented to the plot rectangle. Merges with the multi-key row-stacking layout. Updated theme-legend-rows tests to assert the vertical-stacking decision rather than the whole layout object. ThemeLabR2: wrap cells instead of horizontal scroll per section. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 33e4b998-4b58-4d7e-9cf8-c107b10e45c2 --- packages/flint-js/src/core/theme/ground.ts | 65 +++++++++++---- packages/flint-js/src/vegalite/theme.ts | 82 +++++++++++++++++-- .../flint-js/tests/theme-legend-rows.test.ts | 6 +- site/src/playground/ThemeLabR2.tsx | 2 +- 4 files changed, 130 insertions(+), 25 deletions(-) diff --git a/packages/flint-js/src/core/theme/ground.ts b/packages/flint-js/src/core/theme/ground.ts index 2ddd60e4..9ddbd5b3 100644 --- a/packages/flint-js/src/core/theme/ground.ts +++ b/packages/flint-js/src/core/theme/ground.ts @@ -81,6 +81,8 @@ export interface GroundingContext { subplotHeight: number; xStep: number; yStep: number; + xStepUnit?: 'item' | 'group'; + yStepUnit?: 'item' | 'group'; stepPadding: number; titleFontSize: number; legendFontSize: number; @@ -832,10 +834,10 @@ export function groundTheme(themeIn: ThemeSpec, ctx: GroundingContext): DesignDe // --- data labels -------------------------------------------------------- const dl = theme.dataLabels ?? {}; - const dlPlacement = dl.placement ?? 'outsideMark'; + let dlPlacement = dl.placement ?? 'outsideMark'; // A label placed *on* a mark sits on the mark's fill, whatever the house // said about ink. Contrast is a legibility floor, not a style choice. - const dlInkMode = dl.inkMode + let dlInkMode = dl.inkMode ?? (dlPlacement === 'atMark' ? 'contrastWithMark' : 'fixed'); if (!dl.inkMode && dlInkMode === 'contrastWithMark') { say('dataLabels.inkMode', @@ -910,6 +912,47 @@ export function groundTheme(themeIn: ThemeSpec, ctx: GroundingContext): DesignDe : '`whenTheyFit` resolved to false — no banded axis to key values to'); } } + // A number is printed across a bar's *width*, not up its height. The band + // checks above stand a line of text inside a band; a value laid across a + // vertical bar has to clear the bar's width instead. A grouped bar splits + // its band between the series with nothing between them, so each bar's slot + // is the band over the series count; a single series keeps the whole band + // and can lean a number into the padding on either side. + let valueMaxAbs = 0; + let valueDigits = 1; + { + const mch = bindings.measureChannels[0]; + const field = mch + ? (ctx.channelSemantics[mch]?.field ?? ctx.positional?.[mch]?.field) + : undefined; + if (field) { + for (const row of ctx.table) { + const v = row?.[field]; + if (typeof v !== 'number') continue; + valueMaxAbs = Math.max(valueMaxAbs, Math.abs(v)); + valueDigits = Math.max(valueDigits, String(Math.round(Math.abs(v))).length); + } + } + } + const valueLabelWidthPx = (valueLabel.fontSize ?? 10) * 0.62 * valueDigits + 12; + if (dlShow && bindings.categoricalChannel === 'x' && signals.hasBandedAxis && valueMaxAbs > 0) { + const grouped = ctx.layout.xStepUnit === 'group' && signals.seriesCount > 1; + const slot = grouped + ? ctx.layout.xStep / Math.max(1, signals.seriesCount) + : ctx.layout.xStep; + if (valueLabelWidthPx > slot) { + if (grouped) { + dlShow = false; + say('dataLabels.show', + `the bars group ${signals.seriesCount} to a band — each is ${Math.round(slot)}px wide, too narrow to carry a ${Math.round(valueLabelWidthPx)}px number without it landing on the next bar`); + } else if (dlPlacement === 'atMark') { + dlPlacement = 'outsideMark'; + say('dataLabels.placement', + `the bar is ${Math.round(slot)}px wide but the number is ${Math.round(valueLabelWidthPx)}px — it moves above the bar, where the gaps between bars give it room`); + } + } + } + if (dlShow && legendShow && legendSpec.suppressWhenValuesPrinted) { // A printed value is not a name. It replaces a legend that was itself // a value key — a ramp — but never one that carried series names, and @@ -963,22 +1006,10 @@ export function groundTheme(themeIn: ThemeSpec, ctx: GroundingContext): DesignDe let insideMinValue: number | undefined; let outsideMaxValue: number | undefined; if (dlShow && measureChannel) { - const field = ctx.channelSemantics[measureChannel]?.field ?? ctx.positional?.[measureChannel]?.field; const span = measureChannel === 'x' ? ctx.layout.subplotWidth : ctx.layout.subplotHeight; - let maxAbs = 0; - let digits = 1; - if (field) { - for (const row of ctx.table) { - const v = row?.[field]; - if (typeof v !== 'number') continue; - maxAbs = Math.max(maxAbs, Math.abs(v)); - digits = Math.max(digits, String(Math.round(Math.abs(v))).length); - } - } - if (maxAbs > 0 && span > 0) { - const labelPx = (valueLabel.fontSize ?? 10) * 0.62 * digits + 12; - insideMinValue = (labelPx / span) * maxAbs; - outsideMaxValue = maxAbs - insideMinValue; + if (valueMaxAbs > 0 && span > 0) { + insideMinValue = (valueLabelWidthPx / span) * valueMaxAbs; + outsideMaxValue = valueMaxAbs - insideMinValue; } } diff --git a/packages/flint-js/src/vegalite/theme.ts b/packages/flint-js/src/vegalite/theme.ts index 6f4a89f2..8042e313 100644 --- a/packages/flint-js/src/vegalite/theme.ts +++ b/packages/flint-js/src/vegalite/theme.ts @@ -1773,6 +1773,24 @@ function applyLegend(spec: any, config: any, d: DesignDecisions, table: any[], s } } if (!l.title) config.legend.title = null; + // A top or bottom key is a caption to the whole graphic, so it begins where + // the graphic does — flush with the title down the left edge — not indented + // to the plot rectangle the way Vega-Lite lays it by default. `bounds: + // 'full'` measures the key against the same box the start-anchored title + // uses (axes included), so the two share one left margin; the whitespace an + // indented key leaves between itself and the title, worst when the key + // wraps to several rows, closes up. + if ((l.orient === 'top' || l.orient === 'bottom') + && (l.direction ?? 'horizontal') === 'horizontal') { + config.legend.layout = { + ...(config.legend.layout ?? {}), + [l.orient]: { + anchor: 'start', + bounds: 'full', + ...(config.legend.layout?.[l.orient] ?? {}), + }, + }; + } if (l.orient === 'top-right' || l.orient === 'top-left') { config.legend.fillColor = d.surface.plot; config.legend.padding = 4; @@ -1841,7 +1859,14 @@ function applyLegend(spec: any, config: any, d: DesignDecisions, table: any[], s const block = blockWidth(spec, table); const total = widths.reduce((a, b) => a + b, 0); if (widths.length > 1 && block && total > block) { - config.legend.layout = { [l.orient]: { direction: 'vertical', anchor: 'start' } }; + config.legend.layout = { + ...(config.legend.layout ?? {}), + [l.orient]: { + ...(config.legend.layout?.[l.orient] ?? {}), + direction: 'vertical', + anchor: 'start', + }, + }; say('legend.placement', `${widths.length} keys want ${Math.round(total)}px across a ${block}px block — they take a row each`); } @@ -2216,6 +2241,12 @@ function labelOneBody(spec: any, body: any, d: DesignDecisions, table: any[], sa // 10px dot lands the text on the dot. Where a mark has a radius, the gap // is measured from the rim by adding it. const radius = Math.max(0, ...units.map((unit: any) => markRadius(unit, spec, d))); + // A slice too thin to stand a line of text across gets no label: its share + // of the circle, swung out to the label radius, is a shorter arc than the + // label is tall, so the number would land on its neighbours. Hiding it by + // opacity (not by dropping the row) keeps every slice in the stack, so the + // surviving labels stay on the angles their arcs actually occupy. + let radialLabelKeepTest: string | undefined; const geometry = (within: boolean): any => { const w = reversed ? !within : within; return horizontal @@ -2243,8 +2274,34 @@ function labelOneBody(spec: any, body: any, d: DesignDecisions, table: any[], sa const r = typeof declared === 'number' ? declared : (typeof w === 'number' && typeof h === 'number' ? Math.min(w, h) / 2 : undefined); - if (r) Object.assign(markDef, { radius: inside ? r * 0.72 : r + 14 }); - else say('dataLabels', 'the arc has no radius to hang a label from'); + if (r) { + const labelRadius = inside ? r * 0.72 : r + 14; + Object.assign(markDef, { radius: labelRadius }); + // The slice's share of the circle is its value over the total; swung + // out to the label radius that share becomes an arc of + // `labelRadius · share · 2π`. A label needs at least its own height + // of arc to sit in without touching its neighbours, so the smallest + // labelled value is the one whose arc clears a line of text. + const total = table.reduce((s, row) => { + const v = row?.[measure.field]; + return typeof v === 'number' && Number.isFinite(v) ? s + Math.abs(v) : s; + }, 0); + const minArc = (t.fontSize ?? 10) + 2; + if (total > 0 && labelRadius > 0) { + const minValue = (minArc * total) / (labelRadius * 2 * Math.PI); + if (minValue > 0) { + radialLabelKeepTest = `abs(datum[${JSON.stringify(measure.field)}]) >= ${minValue}`; + const dropped = table.filter((row) => { + const v = row?.[measure.field]; + return typeof v === 'number' && Math.abs(v) < minValue; + }).length; + if (dropped > 0) { + say('dataLabels.show', + `${dropped} slice${dropped === 1 ? '' : 's'} narrower than a line of text go unlabelled — their arc would not hold the number`); + } + } + } + } else say('dataLabels', 'the arc has no radius to hang a label from'); } else { Object.assign(markDef, geometry(inside)); } @@ -2290,6 +2347,9 @@ function labelOneBody(spec: any, body: any, d: DesignDecisions, table: any[], sa } const layer: any = { __themeSynthetic: true, mark: markDef, encoding: labelEncoding }; + if (radialLabelKeepTest) { + labelEncoding.opacity = { condition: { test: radialLabelKeepTest, value: 1 }, value: 0 }; + } appendLayer(body, layer); if (radial) { @@ -2428,9 +2488,21 @@ function appendLayer(body: any, layer: any): void { } function readableOn(background: string, light: string, dark: string): string { + // `light` and `dark` are the theme's two ink candidates. Which one reads on + // a surface is a fact about their contrast with it, not about which the + // theme calls primary — a dark house's "primary" ink is itself light, so a + // fixed light/dark mapping inverts on it. Pick whichever candidate sits + // furthest from the background in luminance; fall back to the first when a + // colour will not parse. const bg = parseColor(background); - if (!bg) return dark; - return luminance(bg) < 0.5 ? light : dark; + if (!bg) return light; + const bgLum = luminance(bg); + const lightC = parseColor(light); + const darkC = parseColor(dark); + if (!lightC || !darkC) return luminance(bg) < 0.5 ? light : dark; + const lightGap = Math.abs(luminance(lightC) - bgLum); + const darkGap = Math.abs(luminance(darkC) - bgLum); + return lightGap >= darkGap ? light : dark; } function growPadding(spec: any, side: 'left' | 'right' | 'top' | 'bottom', amount: number): void { diff --git a/packages/flint-js/tests/theme-legend-rows.test.ts b/packages/flint-js/tests/theme-legend-rows.test.ts index 3ad6c311..00dfe2f3 100644 --- a/packages/flint-js/tests/theme-legend-rows.test.ts +++ b/packages/flint-js/tests/theme-legend-rows.test.ts @@ -66,12 +66,14 @@ describe('two keys above one plot', () => { it('leaves them on one row when the block is wide enough', () => { const spec = bubble(1200); - expect(layoutOf(spec)).toBeUndefined(); + expect(layoutOf(spec)?.top?.direction).toBeUndefined(); + expect(spec._theme.report.some((r: any) => /row each/.test(r.message))).toBe(false); }); it('says nothing about rows when there is only one key', () => { const spec = bubble(320, null); - expect(layoutOf(spec)).toBeUndefined(); + expect(layoutOf(spec)?.top?.direction).toBeUndefined(); + expect(spec._theme.report.some((r: any) => /row each/.test(r.message))).toBe(false); }); /** diff --git a/site/src/playground/ThemeLabR2.tsx b/site/src/playground/ThemeLabR2.tsx index 81690bf5..0f65a05b 100644 --- a/site/src/playground/ThemeLabR2.tsx +++ b/site/src/playground/ThemeLabR2.tsx @@ -65,8 +65,8 @@ function Row({ c }: { c: R2Case }) {
From 066c26f956fd06299be619e9931607473ddf12dc Mon Sep 17 00:00:00 2001 From: Chenglong Wang Date: Thu, 30 Jul 2026 11:25:57 -0700 Subject: [PATCH 024/164] bump: end labels at the last drawn band; landscape panel for readable slopes Series-end labels (seriesEnd/inline placement) ranked each series' rows by the domain field sorted descending, which for an ordinal axis with an explicit sort (months) picks the alphabetically-last value ("Sep"), not the last band drawn ("Dec"). Labels landed mid-plot under the crossing lines. Where the domain carries an explicit sort array, rank by each row's index in it; where the sort is 'descending', rank the field ascending; otherwise keep the descending-field default. Labels now sit in the reserved right margin like a line chart's. Layout: a bump chart has two discrete axes, so the square-cells rule (meant for heatmap tones) pulled both steps equal, giving a tall square panel where every rank change is a near-vertical cliff and twelve crossing series are unreadable. A connected mark is a line, not a grid of cells. For a connected mark with both axes discrete (bars have a continuous value axis; heatmaps declare no cross-section, so neither is caught) skip squaring and instead hold the thin cross axis (rank) to its declared cross-section while stretching the run axis (time) to a bounded 2:1 multiple, clamped to the width budget. bump-many goes 336x336 -> 384x240; the horizontal room untangles the crossings. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 33e4b998-4b58-4d7e-9cf8-c107b10e45c2 --- packages/flint-js/src/core/compute-layout.ts | 43 +++++++++++++++++++- packages/flint-js/src/vegalite/theme.ts | 35 +++++++++++++--- 2 files changed, 72 insertions(+), 6 deletions(-) diff --git a/packages/flint-js/src/core/compute-layout.ts b/packages/flint-js/src/core/compute-layout.ts index e7f57ee8..7b2da77a 100644 --- a/packages/flint-js/src/core/compute-layout.ts +++ b/packages/flint-js/src/core/compute-layout.ts @@ -926,7 +926,48 @@ export function computeLayout( // for that — where squaring would cut the wider step by more than a third — // the grid stays rectangular: a shape nobody asked for is not worth losing // that much room over. - if (xTotalNominalCount > 0 && yTotalNominalCount > 0 && !xHasGrouping && !yHasGrouping) { + // + // A connected mark whose two axes are both discrete — a bump chart, ranks + // over time — is the exception: it is a line, not a grid of cells, so it is + // neither squared nor stretched to fill the height. The mark declares a + // cross-section per axis; the larger one is the run the line travels along + // (time), the smaller the stack it crosses (rank). Squaring the two, or + // letting the rank axis grow one band per competitor, only makes the panel + // taller and every crossing a near-vertical plunge — the shape the eye + // reads worst. Instead the rank axis is held to its thin cross-section and + // the run axis is stretched to a bounded multiple of it, so the panel comes + // out landscape and the slopes sit nearer 45°. Horizontal room is what + // untangles a crossing mass of lines, so the run is where the budget is + // spent. + const isConnectedMark = typeof continuousMarkCrossSection === 'object' + && !!continuousMarkCrossSection.seriesCountAxis; + const bothDiscreteConnected = isConnectedMark + && xTotalNominalCount > 0 && yTotalNominalCount > 0 + && !xHasGrouping && !yHasGrouping; + if (bothDiscreteConnected && typeof continuousMarkCrossSection === 'object') { + const csX = continuousMarkCrossSection.x ?? 0; + const csY = continuousMarkCrossSection.y ?? 0; + if (csX > 0 && csY > 0) { + // Cap the run band's advantage over the cross band: past about 2:1 + // the extra width buys little and the panel just runs off the edge. + const RUN_AR_CAP = 2; + const runIsX = csX >= csY; + const crossCS = runIsX ? csY : csX; + const runBudget = runIsX + ? Math.floor(maxSubplotW / xTotalNominalCount) + : Math.floor(maxSubplotH / yTotalNominalCount); + const cross = Math.max(minStepVal, + Math.min(runIsX ? yStepSize : xStepSize, crossCS)); + const ratio = Math.min(RUN_AR_CAP, Math.max(1, Math.max(csX, csY) / Math.min(csX, csY))); + const run = Math.max( + runIsX ? xStepSize : yStepSize, + Math.min(runBudget, Math.round(cross * ratio))); + if (runIsX) { xStepSize = run; yStepSize = cross; } + else { yStepSize = run; xStepSize = cross; } + } + } + if (xTotalNominalCount > 0 && yTotalNominalCount > 0 && !xHasGrouping && !yHasGrouping + && !bothDiscreteConnected) { const capX = Math.floor(maxSubplotW / xTotalNominalCount); const capY = Math.floor(maxSubplotH / yTotalNominalCount); const generous = Math.round(CELL_BAND_SIZE * Math.max(1, sizeRatio)); diff --git a/packages/flint-js/src/vegalite/theme.ts b/packages/flint-js/src/vegalite/theme.ts index 8042e313..1e6ba4ad 100644 --- a/packages/flint-js/src/vegalite/theme.ts +++ b/packages/flint-js/src/vegalite/theme.ts @@ -2681,12 +2681,37 @@ function applySeriesEndLabels( // list, and a list is labelled at its head, inside the plot. const runs = domain.type === 'quantitative' || domain.type === 'temporal' || domain.type === 'ordinal'; const atEnd = runs || domainChannel === 'x'; + // "The end" is the last band in the order the domain is *drawn*, which for + // an ordinal axis is the order the house declared (Jan…Dec), not the order + // the values happen to sort in as strings. Ranking by the raw field would + // end each series at its alphabetically-last reading — "Sep", stranded + // mid-plot under the crossing lines — instead of at the right-hand edge. So + // where the domain carries an explicit order, rank by each row's position + // in it; otherwise a descending sort of an ordered field finds the last. + let runsRank: any[] = []; + if (runs) { + if (Array.isArray(domain.sort)) { + const arr = JSON.stringify(domain.sort); + const fld = JSON.stringify(domain.field); + runsRank = [ + { calculate: `indexof(${arr}, datum[${fld}])`, as: '__domainOrder' }, + { + window: [{ op: 'row_number', as: '__seriesEndRank' }], + sort: [{ field: '__domainOrder', order: 'descending' }], + groupby: [seriesField], + }, + ]; + } else { + const order = domain.sort === 'descending' ? 'ascending' : 'descending'; + runsRank = [{ + window: [{ op: 'row_number', as: '__seriesEndRank' }], + sort: [{ field: domain.field, order }], + groupby: [seriesField], + }]; + } + } const rank = runs - ? [{ - window: [{ op: 'row_number', as: '__seriesEndRank' }], - sort: [{ field: domain.field, order: 'descending' }], - groupby: [seriesField], - }] + ? runsRank : atEnd ? [ { window: [{ op: 'row_number', as: '__dataOrder' }] }, From 42d369d86282732b1f97641ee1fff3b0f3569674 Mon Sep 17 00:00:00 2001 From: Chenglong Wang Date: Thu, 30 Jul 2026 12:11:32 -0700 Subject: [PATCH 025/164] Themed slope charts: house-aware end labels, legend, and band-step floor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A slopegraph reads in one of two ways, and which one is a house matter. Editorial houses take the value axis away, so the values must be printed on the marks — a "Name Value" label outside each endpoint, no colour key. A house that keeps its measure axis (nature: a journal spine + always-on legend) reads values off the axis and tells the lines apart with the legend, like ordinary Vega-Lite. Printing names on its endpoints only fights the axis for the same margin. - resolveChartDefaults: enable slope end labels (showText/showSeriesInLabel) as a themed default only where the house omits the measure-axis line; a house that keeps its axis is left with its legend. - assemble.ts: create chartProperties for any theme (not only those declaring chartDefaults), or the default never runs. - slope template: drop the now-redundant colour legend once names are on the marks; GroundingContext.namesOnMarks generalises the colour→single-ink drop to any house naming its series on the mark (was hard-keyed to economist). - minBandStep: a chart-specific floor on the per-category band step a house's layout.bandStep may grow but not undercut. slope declares 120, so a compact house (nature's 46px) can't pack the two columns near-vertical with no room for labels. Gates: flint-js tsc clean; 694 vitest; theme-audit 62 sheets 0 FAILED; R2 85 sheets 0 FAILED. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 33e4b998-4b58-4d7e-9cf8-c107b10e45c2 --- packages/flint-js/src/core/theme/ground.ts | 41 +++++++++++++++---- packages/flint-js/src/core/types.ts | 14 +++++++ packages/flint-js/src/vegalite/assemble.ts | 16 ++++++-- .../flint-js/src/vegalite/templates/slope.ts | 14 ++++++- 4 files changed, 72 insertions(+), 13 deletions(-) diff --git a/packages/flint-js/src/core/theme/ground.ts b/packages/flint-js/src/core/theme/ground.ts index 9ddbd5b3..d700dfd6 100644 --- a/packages/flint-js/src/core/theme/ground.ts +++ b/packages/flint-js/src/core/theme/ground.ts @@ -57,6 +57,8 @@ export interface GroundingContext { markChannel: string; /** Mark families present in the instantiated chart, e.g. `['bar','text']`. */ markTypes: string[]; + /** The resolved `showSeriesInLabel`: the chart names its series on the marks. */ + namesOnMarks?: boolean; /** Per-channel resolved semantics (phase 0). */ channelSemantics: Record; /** Encoding types after template-driven conversion (e.g. Q→O for bars). */ @@ -168,8 +170,30 @@ export function resolveChartDefaults( target: Record, ): ThemeReport[] { const defaults = theme?.chartDefaults; - if (!defaults) return []; - const wanted = { ...(defaults['*'] ?? {}), ...(defaults[chartType] ?? {}) }; + // A slopegraph reads in one of two ways, and which one is a house matter. + // An editorial house takes the value axis away — no spine, no ruler — so + // the only place a value can be read is off the mark itself: it prints the + // number at each end, with the series name beside it, and needs no colour + // key. A house that keeps its value axis (a journal panel with a measured + // spine) reads the numbers off that axis and tells the lines apart the + // ordinary way, with a legend — printing a name on every end point there + // would fight the axis for the same margin and clutter a small panel. + // + // So the end-label treatment is the default only where the house omits the + // measure axis line. Houses may still override in their own defaults, a + // caller who set the control keeps it, and baseline (no theme) is left + // alone — it keeps its legend. + const omitsMeasureAxis = theme?.structure?.axis?.measure?.line === 'omit'; + const globalDefaults: Record> = omitsMeasureAxis + ? { 'Slope Chart': { showText: true, showSeriesInLabel: true } } + : {}; + const wanted = { + ...(globalDefaults['*'] ?? {}), + ...(globalDefaults[chartType] ?? {}), + ...(defaults?.['*'] ?? {}), + ...(defaults?.[chartType] ?? {}), + }; + if (Object.keys(wanted).length === 0) return []; const keys = new Set((declared ?? []).map((p) => p.key)); const report: ThemeReport[] = []; for (const [key, value] of Object.entries(wanted)) { @@ -1433,12 +1457,13 @@ function groundSeriesInk( if (s.overflow) { say('ink.series.categorical', `${count} series but the house declares ${categorical.length} — the rest take the overflow ink`); - } else if (theme.chartDefaults?.[ctx.chartType]?.showSeriesInLabel === true) { - // The house prints the series name on the mark for this kind of - // chart, so the names are already on the page in words. Colour was - // never the key here; keeping a foreign palette only spreads seven - // hues across seven lines and seven labels that say the same thing - // the words do. One ink, and the reader reads the names. + } else if (ctx.namesOnMarks === true) { + // The chart prints the series name on the mark (a slopegraph's end + // labels, a house that asked for it), so the names are already on + // the page in words. Colour was never the key here; keeping a + // foreign palette only spreads seven hues across seven lines and + // seven labels that say the same thing the words do. One ink, and + // the reader reads the names. say('ink.series.categorical', `${count} series against ${categorical.length} house inks, but the house names them on the mark — colour stops naming and takes the single ink`); return { ...base, mode: 'single' }; diff --git a/packages/flint-js/src/core/types.ts b/packages/flint-js/src/core/types.ts index a21c997d..6775b623 100644 --- a/packages/flint-js/src/core/types.ts +++ b/packages/flint-js/src/core/types.ts @@ -1236,6 +1236,20 @@ export interface AssembleOptions { * Default: 20. */ defaultBandSize?: number; + /** + * Chart-specific **floor** on the per-category band step (at a 300px + * baseline canvas), which a house's `layout.bandStep` may grow but not + * undercut. Ordinary charts have no such floor: a compact house is right + * to print thin bars. But a few chart types are only legible above a + * minimum band width regardless of house — a slopegraph draws its whole + * meaning from the angle of two columns, and a house that packs them 46px + * apart turns every slope near-vertical and leaves no room for the end + * labels. Such a template states the width its read needs here, and the + * house is held to it as a minimum while still free to spread wider. + * + * Unset for most templates (no floor). Set via paramOverrides. + */ + minBandStep?: number; /** * Maximum pixels per discrete category at a 300px baseline canvas, * scaled proportionally with canvas size (like {@link defaultBandSize}). diff --git a/packages/flint-js/src/vegalite/assemble.ts b/packages/flint-js/src/vegalite/assemble.ts index 63b04fc7..8b2dc375 100644 --- a/packages/flint-js/src/vegalite/assemble.ts +++ b/packages/flint-js/src/vegalite/assemble.ts @@ -148,7 +148,7 @@ export function assembleVegaLite(input: ChartAssemblyInput): any { // left unsmoothed — change what is drawn, not how it is dressed, so they // are folded in here, before the pipeline reads the properties. Anything // the caller stated already is left alone. - if (themeSpec?.chartDefaults && !chartProperties) chartProperties = {}; + if (themeSpec && !chartProperties) chartProperties = {}; const chartDefaultsReport = chartProperties ? resolveChartDefaults( themeSpec, chartType, chartTemplate.properties, @@ -382,13 +382,20 @@ export function assembleVegaLite(input: ChartAssemblyInput): any { const houseBandStep = themeSpec?.layout?.bandStep; const cellGrid = declaration.axisFlags?.x?.banded === true && declaration.axisFlags?.y?.banded === true; + // A template may state a floor its read cannot go below (a slopegraph needs + // its two columns spread wide however compact the house). The house sets + // the band step, but not below that floor. + const minBandStep = (declaration.paramOverrides as AssembleOptions | undefined)?.minBandStep; if (houseBandStep && options.defaultBandSize == null && !cellGrid) { - effectiveOptions.defaultBandSize = houseBandStep; - effectiveOptions.maxBandSize = Math.max(houseBandStep, effectiveOptions.maxBandSize ?? 0); + const step = minBandStep ? Math.max(houseBandStep, minBandStep) : houseBandStep; + effectiveOptions.defaultBandSize = step; + effectiveOptions.maxBandSize = Math.max(step, effectiveOptions.maxBandSize ?? 0); chartDefaultsReport.push({ stage: 'ground', path: 'layout.bandStep', - message: `the house gives each category ${houseBandStep}px`, + message: minBandStep && step > houseBandStep + ? `the house gives each category ${houseBandStep}px, but this chart reads only above ${minBandStep}px — held to ${step}px` + : `the house gives each category ${houseBandStep}px`, }); } else if (houseBandStep && cellGrid) { chartDefaultsReport.push({ @@ -741,6 +748,7 @@ export function assembleVegaLite(input: ChartAssemblyInput): any { chartType, markChannel: chartTemplate.markCognitiveChannel, markTypes, + namesOnMarks: (chartProperties as any)?.showSeriesInLabel === true, channelSemantics, resolvedTypes: declaration.resolvedTypes as Record | undefined, axisFlags: declaration.axisFlags, diff --git a/packages/flint-js/src/vegalite/templates/slope.ts b/packages/flint-js/src/vegalite/templates/slope.ts index 15fe1e3c..2530cf9c 100644 --- a/packages/flint-js/src/vegalite/templates/slope.ts +++ b/packages/flint-js/src/vegalite/templates/slope.ts @@ -82,8 +82,12 @@ export const slopeChartDef: ChartTemplateDef = { paramOverrides: { // Spread the two periods well apart and keep the plot from being // squeezed tall: a wide band step + no series-count vertical - // stretch yields the classic balanced slopegraph framing. + // stretch yields the classic balanced slopegraph framing. The + // band step is also a floor (`minBandStep`): a compact house may + // spread the two columns wider, but not pack them so close the + // slopes go near-vertical and the end labels have no room. defaultBandSize: 120, + minBandStep: 120, continuousMarkCrossSection: { x: 0, y: 0, seriesCountAxis: 'auto' }, facetAspectRatioResistance: 0.4, }, @@ -141,6 +145,14 @@ export const slopeChartDef: ChartTemplateDef = { const seriesField = ctx.channelSemantics?.color?.field ?? ctx.channelSemantics?.detail?.field; const withSeries = props.showSeriesInLabel === true && !!seriesField; + // Names on the marks make the colour legend redundant: it would + // only repeat, in a second place, the words already printed at + // each line's ends. Drop it so the plot is the whole story. + if (withSeries && (spec.encoding as any)?.color) { + (spec.encoding as any).color = { + ...(spec.encoding as any).color, legend: null, + }; + } const fmt = props.labelFormat ?? '.3~s'; const valueExpr = `format(datum[${JSON.stringify(yEnc.field)}], ${JSON.stringify(fmt)})`; const labelExpr = withSeries From 5c9ddf26e8792801b518b4f7adde02901881d87e Mon Sep 17 00:00:00 2001 From: Chenglong Wang Date: Thu, 30 Jul 2026 13:20:16 -0700 Subject: [PATCH 026/164] Stacked-area band labels: stack the text, and end at the real last reading MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two faults in the name-in-the-band labels a house knocks out of a stacked area at its last reading. 1. The band is an `area`, which Vega-Lite stacks on its own; the label is a `text`, which it does not. Left implicit, every name landed at its series' *raw* value and the whole set piled up at the foot of the plot — the stack might as well not have been there. Naming the band's own offset on the label layer lifts each name to the middle of the band it belongs to. 2. "The end" was found by sorting the domain descending. On a bare nominal index ("Stage 1"…"Stage 12") that sorts as strings, so every series ended at "Stage 9" — the label sat mid-plot AND printed Stage 9's number, not Stage 12's. Rank by declared order where the house gives one, by the field where it is a time or number line, and by arrival order for a plain nominal domain (matching applySeriesEndLabels). The name now sits at the true last column, whose point-scale position is the plot's right edge, so an outside name clears the plot without a fragile width expression that a concatenated view (economist's rule header) resolves against the wrong signal. Gates: flint-js tsc clean; 694 vitest; theme-audit 62 sheets 0 FAILED; R2 85 sheets 0 FAILED. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 33e4b998-4b58-4d7e-9cf8-c107b10e45c2 --- packages/flint-js/src/vegalite/theme.ts | 36 ++++++++++++++++++++----- 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/packages/flint-js/src/vegalite/theme.ts b/packages/flint-js/src/vegalite/theme.ts index 1e6ba4ad..adc7bce7 100644 --- a/packages/flint-js/src/vegalite/theme.ts +++ b/packages/flint-js/src/vegalite/theme.ts @@ -2904,17 +2904,41 @@ function bandEndLabels( const name = normalized ? `datum[${JSON.stringify(seriesField)}] + ''` : `datum[${JSON.stringify(seriesField)}] + ' ' + ${shown}`; + // The band is an `area` mark, which Vega-Lite stacks on its own; a `text` + // mark is not stacked unless told to. Left implicit, every label lands at + // its series' *raw* reading and the whole set piles up at the foot of the + // plot as if nothing were stacked. Naming the band's own offset on the + // label layer lifts each name to the middle of the band it belongs to. + const stackOffset = + value.stack === null || value.stack === false || value.stack === 'none' + ? undefined + : (value.stack ?? 'zero'); // Both layers keep every series, because a stacked layer stacks only the // rows it is given: drop one and the rest slide off their own bands. What // changes between them is whether the text says anything. + // + // "The end" is the last reading in the order the domain is *drawn*. A time + // or number line sorts itself; an explicit category order is read off the + // house's list; but a bare nominal domain ("Stage 1"…"Stage 12") has no + // order but the one its rows arrived in — and sorting those as *strings* + // ends every series at "Stage 9", stranded mid-plot, not at "Stage 12". So + // rank by declared order where there is one, by the field where it is a + // scale, and by arrival order otherwise. + const rankTf: any[] = Array.isArray(domain.sort) + ? [ + { calculate: `indexof(${JSON.stringify(domain.sort)}, datum[${JSON.stringify(domain.field)}])`, as: '__bandDomainOrder' }, + { window: [{ op: 'row_number', as: '__bandEndRank' }], sort: [{ field: '__bandDomainOrder', order: 'descending' }], groupby: [seriesField] }, + ] + : (domain.type === 'quantitative' || domain.type === 'temporal') + ? [{ window: [{ op: 'row_number', as: '__bandEndRank' }], sort: [{ field: domain.field, order: domain.sort === 'descending' ? 'ascending' : 'descending' }], groupby: [seriesField] }] + : [ + { window: [{ op: 'row_number', as: '__bandDataOrder' }] }, + { window: [{ op: 'row_number', as: '__bandEndRank' }], sort: [{ field: '__bandDataOrder', order: 'descending' }], groupby: [seriesField] }, + ]; const endLayer = (inside: boolean): any => ({ __themeSynthetic: true, transform: [ - { - window: [{ op: 'row_number', as: '__bandEndRank' }], - sort: [{ field: domain.field, order: 'descending' }], - groupby: [seriesField], - }, + ...rankTf, { filter: 'datum.__bandEndRank === 1' }, { calculate: outside.length ? `${belongs(inside)} ? ${name} : ''` : name, @@ -2933,7 +2957,7 @@ function bandEndLabels( }, encoding: { [domainChannel]: stripAxis(domain), - [valueChannel]: { ...stripAxis(value), bandPosition: 0.5 }, + [valueChannel]: { ...stripAxis(value), bandPosition: 0.5, ...(stackOffset ? { stack: stackOffset } : {}) }, text: { field: '__bandEndLabel', type: 'nominal' }, ...(inside ? knockedOut : inSeriesInk), }, From a1b7d02801ef237bd9ada2581c462ffd1851011c Mon Sep 17 00:00:00 2001 From: Chenglong Wang Date: Thu, 30 Jul 2026 13:28:20 -0700 Subject: [PATCH 027/164] Themed bars on a time axis take the house gap (width band), not fill the step MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A bar's band occupancy is a scale decision — express it as `paddingInner` and grouped and simple bars stay consistent. But that padding only exists on a *band* scale. Put the bars on a continuous time axis (a month per bar) and there is no band scale to pad: the walk found no discrete channel, applied nothing, and the template's pinned pixel width was left to fill — or, past enough points, overrun — the step, so the bars touched. Every house's gap was silently dropped on exactly the temporal bar charts that most look like a run of discrete readings. Read the same occupancy off the continuous side instead: `width`/`height` {band: bandFraction} cuts each bar to its house fraction of the step it spans, and the pinned size is dropped so it cannot fight the band. Guarded to a *time* index with a measure opposite it — a quantitative x is a histogram, whose bars are meant to meet, and is left alone. Gates: flint-js tsc clean; 694 vitest; theme-audit 62 sheets 0 FAILED; R2 85 sheets 0 FAILED (histograms still meet, discrete bars unchanged). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 33e4b998-4b58-4d7e-9cf8-c107b10e45c2 --- packages/flint-js/src/vegalite/theme.ts | 26 +++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/packages/flint-js/src/vegalite/theme.ts b/packages/flint-js/src/vegalite/theme.ts index adc7bce7..2b31ffc9 100644 --- a/packages/flint-js/src/vegalite/theme.ts +++ b/packages/flint-js/src/vegalite/theme.ts @@ -990,6 +990,32 @@ function applyMarks(spec: any, d: DesignDecisions, table: any[], say: (p: string } } } + // A bar on a *continuous* index — a month on a time axis — has + // no band scale to pad, so the same occupancy is expressed as a + // fraction of the step the mark spans: `width`/`height` {band} + // on the continuous side. Without it the template's pinned pixel + // size fills — or, over enough points, overruns — the step and + // the bars touch, losing the gap the house asked for. Only a + // time index bands this way; a quantitative x is a histogram, + // whose bars are meant to meet. + if (mark === 'bar' && discrete.length === 0) { + const timeIndex = (['x', 'y'] as const).find((channel) => { + const other = channel === 'x' ? 'y' : 'x'; + return enc[channel]?.field && enc[channel]?.type === 'temporal' + && enc[other]?.field && (enc[other]?.type === 'quantitative' || !!enc[other]?.aggregate); + }); + if (timeIndex) { + const mk = normalizeMark(node.mark); + delete (mk as any).size; + (mk as any)[timeIndex === 'x' ? 'width' : 'height'] = { band: m.bandFraction }; + node.mark = mk; + if (!saidBand) { + say('marks.bandFraction', + `bars on a continuous time index are cut to ${Math.round(m.bandFraction * 100)}% of the step, not left to fill it`); + saidBand = true; + } + } + } } } // A box is a summary, and how much of its band it fills is a house From e0ff6dffa0a69d09dd03692947d70b1ecdd44cae Mon Sep 17 00:00:00 2001 From: Chenglong Wang Date: Thu, 30 Jul 2026 13:30:56 -0700 Subject: [PATCH 028/164] Revert "Themed bars on a time axis take the house gap (width band), not fill the step" This reverts commit a1b7d02801ef237bd9ada2581c462ffd1851011c. --- packages/flint-js/src/vegalite/theme.ts | 26 ------------------------- 1 file changed, 26 deletions(-) diff --git a/packages/flint-js/src/vegalite/theme.ts b/packages/flint-js/src/vegalite/theme.ts index 2b31ffc9..adc7bce7 100644 --- a/packages/flint-js/src/vegalite/theme.ts +++ b/packages/flint-js/src/vegalite/theme.ts @@ -990,32 +990,6 @@ function applyMarks(spec: any, d: DesignDecisions, table: any[], say: (p: string } } } - // A bar on a *continuous* index — a month on a time axis — has - // no band scale to pad, so the same occupancy is expressed as a - // fraction of the step the mark spans: `width`/`height` {band} - // on the continuous side. Without it the template's pinned pixel - // size fills — or, over enough points, overruns — the step and - // the bars touch, losing the gap the house asked for. Only a - // time index bands this way; a quantitative x is a histogram, - // whose bars are meant to meet. - if (mark === 'bar' && discrete.length === 0) { - const timeIndex = (['x', 'y'] as const).find((channel) => { - const other = channel === 'x' ? 'y' : 'x'; - return enc[channel]?.field && enc[channel]?.type === 'temporal' - && enc[other]?.field && (enc[other]?.type === 'quantitative' || !!enc[other]?.aggregate); - }); - if (timeIndex) { - const mk = normalizeMark(node.mark); - delete (mk as any).size; - (mk as any)[timeIndex === 'x' ? 'width' : 'height'] = { band: m.bandFraction }; - node.mark = mk; - if (!saidBand) { - say('marks.bandFraction', - `bars on a continuous time index are cut to ${Math.round(m.bandFraction * 100)}% of the step, not left to fill it`); - saidBand = true; - } - } - } } } // A box is a summary, and how much of its band it fills is a house From b3880f6ffe04f6580e3162deb7206ab6d14a7aa9 Mon Sep 17 00:00:00 2001 From: Chenglong Wang Date: Thu, 30 Jul 2026 13:42:57 -0700 Subject: [PATCH 029/164] Themed bars on a continuous-banded axis: re-cut the layout's size to the house gap A house's marks.bandFraction reaches a discrete bar as scale paddingInner, but a bar on a continuous-banded axis (a date, a year) has no band scale to carry it. The layout already cuts that bar to step x CONTINUOUS_BAR_STEP_FILL (0.9), capped by a min-gap guard so the closest pair never touches. The theme now rescales that collision-safe pixel size by bandFraction / 0.9. Every house asks for a thinner bar than the baseline, so re-cutting can only widen the gap, never collide -- even on irregular spacing, which is why the earlier mark.width {band} attempt was reverted. Binned (histogram) bars keep meeting. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 33e4b998-4b58-4d7e-9cf8-c107b10e45c2 --- .../flint-js/src/vegalite/templates/utils.ts | 9 ++++- packages/flint-js/src/vegalite/theme.ts | 34 +++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/packages/flint-js/src/vegalite/templates/utils.ts b/packages/flint-js/src/vegalite/templates/utils.ts index db3ffeea..7a0d353e 100644 --- a/packages/flint-js/src/vegalite/templates/utils.ts +++ b/packages/flint-js/src/vegalite/templates/utils.ts @@ -206,6 +206,13 @@ export function alignStackOrderToColorOrder(spec: any, ctx: InstantiateContext): * Adjust bar/rect marks for continuous-as-discrete axes. * v2 version: reads layout info from InstantiateContext. */ +/** + * Fraction of a continuous-banded step a bar fills by default, leaving a 10% + * gap. A house that states its own `marks.bandFraction` re-cuts against this + * baseline (see theme.ts `bandWalk`), so the two must agree on the number. + */ +export const CONTINUOUS_BAR_STEP_FILL = 0.9; + export function adjustBarMarks(spec: any, ctx: InstantiateContext): void { const layout = ctx.layout; for (const axis of ['x', 'y'] as const) { @@ -234,7 +241,7 @@ export function adjustBarMarks(spec: any, ctx: InstantiateContext): void { const maxSize = enc?.field ? maxNonOverlapSize(enc.field, ctx.table, isTemporal, subplotDim, count) : Infinity; - const cellSize = Math.max(2, Math.min(Math.round(effStep * 0.9), maxSize)); + const cellSize = Math.max(2, Math.min(Math.round(effStep * CONTINUOUS_BAR_STEP_FILL), maxSize)); if (Array.isArray(spec.layer)) { for (const layer of spec.layer) { diff --git a/packages/flint-js/src/vegalite/theme.ts b/packages/flint-js/src/vegalite/theme.ts index adc7bce7..cdf2e6a6 100644 --- a/packages/flint-js/src/vegalite/theme.ts +++ b/packages/flint-js/src/vegalite/theme.ts @@ -16,6 +16,7 @@ import type { DesignDecisions, ThemeReport } from '../core/theme/types.js'; import { contrastingInk, parseColor, luminance, toHex } from '../core/theme/presence.js'; +import { CONTINUOUS_BAR_STEP_FILL } from './templates/utils.js'; /** Mark families that carry data values (as opposed to chrome). */ const DATA_MARKS = new Set([ @@ -970,6 +971,26 @@ function applyMarks(spec: any, d: DesignDecisions, table: any[], say: (p: string 'the marks are cells in a grid, not bars in a row — band occupancy is a bar rule and does not apply'); saidCells = true; } + } else if (mark === 'bar' && discrete.length === 0 + && typeof (node.mark as any)?.size === 'number' + && continuousBandedBar(enc)) { + // A bar on a continuous-banded axis (a year, a date) has no band + // scale to carry `paddingInner`, so the pixel width the layout + // already cut — `step × CONTINUOUS_BAR_STEP_FILL`, capped so the + // closest pair never touches — is the only handle. Re-scale it + // to the house's fill: dividing out the baseline recovers the + // step, and every house asks for a thinner bar than the + // baseline, so re-cutting can only widen the gap, never collide. + const current = (node.mark as any).size as number; + const resized = Math.max(1, Math.round(current * m.bandFraction / CONTINUOUS_BAR_STEP_FILL)); + if (resized !== current) { + node.mark = { ...normalizeMark(node.mark), size: resized }; + if (!saidBand) { + say('marks.bandFraction', + `bars on a continuous axis are re-cut to ${Math.round(m.bandFraction * 100)}% of the step`); + saidBand = true; + } + } } else { for (const channel of discrete) { const target = node.encoding?.[channel] ?? inherited?.[channel]; @@ -1358,6 +1379,19 @@ function bandStep(spec: any, node: any, enc: any, channel: 'x' | 'y', table: any return size / count; } +/** + * A bar sitting on a continuous positional axis the layout has banded (a year, + * a date): one axis carries a temporal or quantitative field that is not + * binned. There is no band scale to take `paddingInner`, so the only handle on + * the gap is the pixel width the layout already cut. + */ +function continuousBandedBar(enc: any): boolean { + return (['x', 'y'] as const).some((c) => { + const e = enc?.[c]; + return e?.field && (e.type === 'temporal' || e.type === 'quantitative') && !e.bin; + }); +} + /** Side-by-side lanes inside one band, from the offset channel if there is one. */ function laneCount(node: any, enc: any, table: any[]): number { const offset = enc?.xOffset ?? enc?.yOffset; From 3e3b18c7af6c004848fca2080b7098c516c68af3 Mon Sep 17 00:00:00 2001 From: Chenglong Wang Date: Thu, 30 Jul 2026 14:41:49 -0700 Subject: [PATCH 030/164] Themed dodged bars take the house gap on the lane, not just the group A house's marks.bandFraction reached only the group's band scale, so a dodged bar's lanes stayed the width the template cut and butted together. Worse, a locally-dodged bar (a pinned per-lane pixel size) hit the simple pinned-size re-cut, which recomputed size = step x bandFraction from the group step and spread one bar across the whole band -- the lanes overlapped across band boundaries. Now the gap follows the dodge to whichever scale carries it: - native dodge (nominal xOffset/yOffset): paddingInner on the offset band scale, so the lanes get the same fill as the group. - local dodge (quantitative __off + pinned size): re-scale the pinned size by bandFraction / LOCAL_DODGE_LANE_FILL, the template's own lane fill, shared as a constant so the two can't drift. Thinner than the baseline, so it can only widen the lane gap, never overlap. Simple pinned bars keep their step x bandFraction re-cut; boxplots keep their own widthFraction and are left out of the offset path. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 33e4b998-4b58-4d7e-9cf8-c107b10e45c2 --- .../flint-js/src/vegalite/templates/bar.ts | 15 ++++- packages/flint-js/src/vegalite/theme.ts | 59 +++++++++++++++---- 2 files changed, 59 insertions(+), 15 deletions(-) diff --git a/packages/flint-js/src/vegalite/templates/bar.ts b/packages/flint-js/src/vegalite/templates/bar.ts index 19e47cf5..7d9c66eb 100644 --- a/packages/flint-js/src/vegalite/templates/bar.ts +++ b/packages/flint-js/src/vegalite/templates/bar.ts @@ -14,6 +14,14 @@ import { resolveAsDiscrete, alignStackOrderToColorOrder, } from './utils'; +/** + * Fraction of a lane's pitch a locally-dodged bar fills, leaving a small gap + * between the bars inside one band. A house that states its own + * `marks.bandFraction` re-cuts against this baseline (see theme.ts `bandWalk`), + * so the two must agree on the number. + */ +export const LOCAL_DODGE_LANE_FILL = 0.85; + const HEATMAP_SCHEME_COLORS: Record = { viridis: ['#440154', '#fde725'], inferno: ['#000004', '#fcffa4'], @@ -293,11 +301,12 @@ export const groupedBarChartDef: ChartTemplateDef = { { joinaggregate: [{ op: 'distinct', field: groupField, as: '__localCount' }], groupby: [axisField] }, { calculate: `((datum.__laneIdx - 1) - (datum.__localCount - 1) / 2) / ${maxPB}`, as: '__off' }, ]; - // Constant bar width ≈ 85% of a lane. VL's band reserves ~20% - // padding, so the usable per-lane pitch is (band·0.8 / maxPerBand). + // Constant bar width ≈ LOCAL_DODGE_LANE_FILL of a lane. VL's + // band reserves ~20% padding, so the usable per-lane pitch is + // (band·0.8 / maxPerBand). const band = offsetCh === 'xOffset' ? ctx.layout?.xStep : ctx.layout?.yStep; if (band) { - spec.mark = setMarkProp(spec.mark, 'size', Math.max(2, Math.round((band * 0.8 / maxPB) * 0.85))); + spec.mark = setMarkProp(spec.mark, 'size', Math.max(2, Math.round((band * 0.8 / maxPB) * LOCAL_DODGE_LANE_FILL))); } } } diff --git a/packages/flint-js/src/vegalite/theme.ts b/packages/flint-js/src/vegalite/theme.ts index cdf2e6a6..c2e1c354 100644 --- a/packages/flint-js/src/vegalite/theme.ts +++ b/packages/flint-js/src/vegalite/theme.ts @@ -17,6 +17,7 @@ import type { DesignDecisions, ThemeReport } from '../core/theme/types.js'; import { contrastingInk, parseColor, luminance, toHex } from '../core/theme/presence.js'; import { CONTINUOUS_BAR_STEP_FILL } from './templates/utils.js'; +import { LOCAL_DODGE_LANE_FILL } from './templates/bar.js'; /** Mark families that carry data values (as opposed to chrome). */ const DATA_MARKS = new Set([ @@ -996,18 +997,52 @@ function applyMarks(spec: any, d: DesignDecisions, table: any[], say: (p: string const target = node.encoding?.[channel] ?? inherited?.[channel]; if (!target) continue; target.scale = { ...(target.scale ?? {}), paddingInner }; - // A template that pinned its bars to a pixel width fixed the - // gap along with it. The house sets the gap, so the width - // has to move with it. - if (typeof (node.mark as any)?.size === 'number' && mark !== 'boxplot') { - const step = bandStep(spec, node, enc, channel, table); - if (step) { - node.mark = { ...normalizeMark(node.mark), size: Math.max(1, Math.round(step * m.bandFraction)) }; - if (!saidBand) { - say('marks.bandFraction', - `the template pinned its bars to a pixel width — they are re-cut to ${Math.round(m.bandFraction * 100)}% of the band`); - saidBand = true; - } + } + // Dodged bars carry a second band inside each group — the + // offset. A house gap that only narrows the group leaves the + // lanes touching, so the offset has to take the same fill. + const offset = enc.xOffset ?? enc.yOffset; + const offsetCh = enc.xOffset?.field ? 'xOffset' : enc.yOffset?.field ? 'yOffset' : undefined; + const offsetDiscrete = !!offsetCh && (offset?.type === 'nominal' || offset?.type === 'ordinal'); + const offsetLocal = !!offsetCh && offset?.type === 'quantitative'; + const sizedMark = typeof (node.mark as any)?.size === 'number' && mark !== 'boxplot'; + if (offsetDiscrete && mark !== 'boxplot') { + // Native dodge: the lanes are a band scale of their own, so + // the gap lives on the offset scale, not on any pixel size. + const offTarget = node.encoding?.[offsetCh!] ?? inherited?.[offsetCh!]; + if (offTarget) { + offTarget.scale = { ...(offTarget.scale ?? {}), paddingInner }; + if (!saidBand) { + say('marks.bandFraction', + `dodged lanes fill ${Math.round(m.bandFraction * 100)}% of their slot`); + saidBand = true; + } + } + } else if (sizedMark && offsetLocal) { + // Local dodge: the template pinned each lane to + // LOCAL_DODGE_LANE_FILL of its pitch. Re-scale to the house's + // fill — recomputing from the band step would spread one + // bar across the whole band and overlap the lanes. + const current = (node.mark as any).size as number; + const resized = Math.max(1, Math.round(current * m.bandFraction / LOCAL_DODGE_LANE_FILL)); + if (resized !== current) { + node.mark = { ...normalizeMark(node.mark), size: resized }; + if (!saidBand) { + say('marks.bandFraction', + `dodged lanes are re-cut to ${Math.round(m.bandFraction * 100)}% of their pitch`); + saidBand = true; + } + } + } else if (sizedMark && discrete[0]) { + // A simple bar pinned to a pixel width fixed the gap along + // with it. The house sets the gap, so the width moves too. + const step = bandStep(spec, node, enc, discrete[0], table); + if (step) { + node.mark = { ...normalizeMark(node.mark), size: Math.max(1, Math.round(step * m.bandFraction)) }; + if (!saidBand) { + say('marks.bandFraction', + `the template pinned its bars to a pixel width — they are re-cut to ${Math.round(m.bandFraction * 100)}% of the band`); + saidBand = true; } } } From a6cbff68be2c3c93bac078c09c7dfdc0bf02a081 Mon Sep 17 00:00:00 2001 From: Chenglong Wang Date: Thu, 30 Jul 2026 15:15:55 -0700 Subject: [PATCH 031/164] Datawrapper separator: skip the edge stroke when bars are too thin to hold it Datawrapper holds its bars apart with a 1.5px white stroke on the edge, half of which is taken out of the fill on each side. A ramp dodged into fifty lanes (grouped-color-continuous) leaves ~1.5px bars, so the stroke painted over the whole series and the datawrapper panel came up blank. Estimate each bar's extent -- a pinned size, or the category band split across its dodge lanes -- and leave the separator off when it is under twice the stroke width; at that density the packing already reads as separation. Normal bars and histograms (wide enough) keep the signature stroke. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 33e4b998-4b58-4d7e-9cf8-c107b10e45c2 --- packages/flint-js/src/vegalite/theme.ts | 37 +++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/packages/flint-js/src/vegalite/theme.ts b/packages/flint-js/src/vegalite/theme.ts index c2e1c354..42bd3e54 100644 --- a/packages/flint-js/src/vegalite/theme.ts +++ b/packages/flint-js/src/vegalite/theme.ts @@ -1093,6 +1093,9 @@ function applyMarks(spec: any, d: DesignDecisions, table: any[], say: (p: string } if (m.separator?.show) { + const plotW = spec.config?.view?.continuousWidth ?? spec._width ?? 300; + const plotH = spec.config?.view?.continuousHeight ?? spec._height ?? 300; + let saidThin = false; walk(spec, (node) => { const mark = markTypeOf(node.mark); if (mark !== 'bar' && mark !== 'rect') return; @@ -1101,6 +1104,20 @@ function applyMarks(spec: any, d: DesignDecisions, table: any[], say: (p: string // and its fill is the reading, so how it is held apart is its own // decision, taken below. if (isGridCell(node, node.encoding ?? {})) return; + // A separator is drawn on the bar's edge, so half its width is taken + // out of the fill on each side. Once the bars are thinner than the + // stroke — a ramp dodged into fifty lanes, say — the stroke paints + // over the whole bar and the series vanishes. Leave those alone; the + // density itself already reads as separation. + const barW = estimateBarExtent(node, node.encoding ?? {}, table, plotW, plotH); + if (barW < 2 * m.separator!.width) { + if (!saidThin) { + say('marks.separator', + `bars are ${barW.toFixed(1)}px — too thin to hold a ${m.separator!.width}px separator, which would paint over them; left flush`); + saidThin = true; + } + return; + } node.mark = { ...normalizeMark(node.mark), stroke: m.separator!.color, strokeWidth: m.separator!.width }; }); } @@ -1437,6 +1454,26 @@ function laneCount(node: any, enc: any, table: any[]): number { return Math.max(1, distinctCount(table, offset.field) || 1); } +/** + * Roughly how many pixels wide a single bar ends up. A pinned pixel size is the + * answer outright; otherwise the band the layout gives each category is split + * between the dodge lanes inside it. Only an estimate — it decides whether a + * mark is too thin to carry an edge stroke, where the exact figure does not + * matter, only the order of magnitude. + */ +function estimateBarExtent(node: any, enc: any, table: any[], plotW: number, plotH: number): number { + const size = (node.mark as any)?.size; + if (typeof size === 'number') return size; + const discreteX = enc.x?.field && (enc.x.type === 'nominal' || enc.x.type === 'ordinal'); + const discreteY = enc.y?.field && (enc.y.type === 'nominal' || enc.y.type === 'ordinal'); + const channel = discreteX ? 'x' : discreteY ? 'y' : undefined; + if (!channel) return Infinity; + const span = channel === 'x' ? plotW : plotH; + const cats = distinctCount(table, enc[channel].field) || 1; + const lanes = laneCount(node, enc, table); + return span / (cats * lanes); +} + // --------------------------------------------------------------------------- // Series ink // --------------------------------------------------------------------------- From a86a36aee11ae82d834a003e4b04a8f1f776d3d5 Mon Sep 17 00:00:00 2001 From: Chenglong Wang Date: Thu, 30 Jul 2026 15:38:20 -0700 Subject: [PATCH 032/164] Loosen dodged boxplot lane fill so grouped boxes separate Grouped boxplot lanes filled 0.85 of their pitch, so a quartet of 9px boxes in 10px lanes left only ~1px between neighbours and read as one solid multi-colour block (against ~11px between-group gaps). Drop GROUPED_BOXPLOT_LANE_FILL to 0.7 so each dodged box leaves a legible ~30%-of-lane channel; whiskers, medians and outliers still co-centre on the lane (verified via scenegraph). Ungrouped boxes (BOXPLOT_BAND_FILL) are unchanged. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 33e4b998-4b58-4d7e-9cf8-c107b10e45c2 --- packages/flint-js/src/vegalite/templates/scatter.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/flint-js/src/vegalite/templates/scatter.ts b/packages/flint-js/src/vegalite/templates/scatter.ts index 1222a2c7..d5dafeec 100644 --- a/packages/flint-js/src/vegalite/templates/scatter.ts +++ b/packages/flint-js/src/vegalite/templates/scatter.ts @@ -15,7 +15,10 @@ const isDiscreteType = (t: string | undefined) => t === 'nominal' || t === 'ordi // fills most of its category band; a grouped (dodged) box fills most of its // per-subgroup lane. The remainder becomes the gap between adjacent boxes. const BOXPLOT_BAND_FILL = 0.7; -const GROUPED_BOXPLOT_LANE_FILL = 0.85; +// A dodged box should leave a legible gap between adjacent lanes, otherwise a +// quartet of boxes reads as one solid multi-colour block. 0.7 keeps the box +// substantial while opening a clear ~30%-of-lane channel between neighbours. +const GROUPED_BOXPLOT_LANE_FILL = 0.7; // Half-width of the raw-observation jitter cloud, as a fraction of one lane. // 0.3 spreads the points across the middle ~60% of the lane, so the cloud sits // inside its box rather than spilling over the neighbouring one. From 478c63b76c7ff088720eab679c2bab2c4cc2fe01 Mon Sep 17 00:00:00 2001 From: Chenglong Wang Date: Thu, 30 Jul 2026 15:49:32 -0700 Subject: [PATCH 033/164] Relax grouped-box lane-fill assertions to the new 0.7 target The dodged boxplot lane fill dropped to 0.7 so quartets separate; the tests still asserted the old >=0.75 lower bound. Loosen to >=0.6 (box still fills most of its lane, leaves a legible gap) while keeping the "< pitch" no-overlap guard. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 33e4b998-4b58-4d7e-9cf8-c107b10e45c2 --- packages/flint-js/tests/boxplot-grouped-dodge.test.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/flint-js/tests/boxplot-grouped-dodge.test.ts b/packages/flint-js/tests/boxplot-grouped-dodge.test.ts index 5cf1e607..35150590 100644 --- a/packages/flint-js/tests/boxplot-grouped-dodge.test.ts +++ b/packages/flint-js/tests/boxplot-grouped-dodge.test.ts @@ -73,7 +73,9 @@ describe('grouped boxplot dodging', () => { makeGroupedBoxplotInput(['Electronics', 'Clothing', 'Food'], ['Male', 'Female']), ) as any; const pitch = lanePitch(grouped, subgroups); - expect(sizeOf(grouped) / pitch).toBeGreaterThanOrEqual(0.75); + // The box fills most of its lane but leaves a legible gap (~30%) between + // neighbours so a quartet of boxes does not read as one solid block. + expect(sizeOf(grouped) / pitch).toBeGreaterThanOrEqual(0.6); expect(sizeOf(grouped)).toBeLessThan(pitch); }); @@ -94,11 +96,11 @@ describe('grouped boxplot dodging', () => { // Each sub-lane (and thus each box) shrinks as subgroups are added. expect(stepOf(four) / 4).toBeLessThan(stepOf(two) / 2); // Boxes never exceed their lane pitch (no within-group overlap) yet still - // fill most of it at both subgroup counts. + // fill most of it (leaving a legible gap) at both subgroup counts. expect(sizeOf(two)).toBeLessThan(lanePitch(two, 2)); expect(sizeOf(four)).toBeLessThan(lanePitch(four, 4)); - expect(sizeOf(two) / lanePitch(two, 2)).toBeGreaterThanOrEqual(0.75); - expect(sizeOf(four) / lanePitch(four, 4)).toBeGreaterThanOrEqual(0.75); + expect(sizeOf(two) / lanePitch(two, 2)).toBeGreaterThanOrEqual(0.6); + expect(sizeOf(four) / lanePitch(four, 4)).toBeGreaterThanOrEqual(0.6); }); it('uses yOffset when the categorical axis is y (horizontal boxplot)', () => { From 7964b7f3102bab93e9e4479d037e8e9cfc9e82fb Mon Sep 17 00:00:00 2001 From: Chenglong Wang Date: Thu, 30 Jul 2026 15:49:32 -0700 Subject: [PATCH 034/164] Drop the category-axis spine on bar tables MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A bar table prints every value in its own column, so the bar is a secondary in-cell glyph and the category axis is a row-header gutter, not a base the bars stand on. Houses that draw a rule under the categories (nyt, economist, nature, datawrapper) were stacking that spine next to the diverging zero rule — two vertical lines where one is furniture. Ground the category axis of a table chart like the no-measure grid case: omit both the domain line and its ticks (floating ticks with no spine look orphaned). The zero rule, where a measure diverges, still stands. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 33e4b998-4b58-4d7e-9cf8-c107b10e45c2 --- packages/flint-js/src/core/theme/ground.ts | 23 +++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/packages/flint-js/src/core/theme/ground.ts b/packages/flint-js/src/core/theme/ground.ts index d700dfd6..2d6ea192 100644 --- a/packages/flint-js/src/core/theme/ground.ts +++ b/packages/flint-js/src/core/theme/ground.ts @@ -305,6 +305,10 @@ const FACET_CHANNELS = ['column', 'row', 'facet']; // the same label escape — a printed value names a quantity they were chosen not // to reduce to. (A box plot itself is caught earlier by its `boxplot` mark.) const DISTRIBUTION_SHAPE_CHARTS = new Set(['Violin Plot', 'Density Plot']); +// A table with in-row bars: every value is also printed in its own column, so +// the bar is a secondary in-cell glyph and the category axis is a row-header +// gutter, not a base the bars stand on. +const TABLE_CHARTS = new Set(['Bar Table']); function distinctCount(table: any[], field: string | undefined): number { if (!field) return 0; @@ -625,15 +629,24 @@ export function groundTheme(themeIn: ThemeSpec, ctx: GroundingContext): DesignDe // there is nothing standing on it, and the rule is just a line under a // list of names. const standsOnIt = bindings.measureChannels.length > 0; - const domain = indexing && !standsOnIt + // A bar table prints each value in its own column, so its category axis + // is a row-header gutter, not a base the bars stand on — like the + // no-measure grid above, a rule under the names is a line under a list. + const tableGutter = indexing && TABLE_CHARTS.has(ctx.chartType); + const domain = (indexing && !standsOnIt) || tableGutter ? rule('omit', structureInk.axis, 'omit') : rule(lineSpec?.line, structureInk.axis, indexing ? 'full' : 'omit'); - if (indexing && !standsOnIt && (lineSpec?.line ?? 'full') !== 'omit') { - say(`structure.axis.categorical.line`, - 'no axis carries a measure — the cells are the structure, and a rule under their names is a line under nothing'); + if (((indexing && !standsOnIt) || tableGutter) && (lineSpec?.line ?? 'full') !== 'omit') { + say(`structure.axis.categorical.line`, tableGutter + ? 'a bar table prints its values in a column — the category axis is a row-header gutter, not a base, so a rule under the names is a line under a list' + : 'no axis carries a measure — the cells are the structure, and a rule under their names is a line under nothing'); } const tickLen = spec?.tickLength === 'long' ? 5 : spec?.tickLength === 'short' ? 2 : 3; - const ticksRule = rule(spec?.ticks, structureInk.axis, 'omit'); + // A row-header gutter has neither spine nor ticks — dropping the line + // but keeping ticks leaves them floating against nothing. + const ticksRule = tableGutter + ? rule('omit', structureInk.axis, 'omit') + : rule(spec?.ticks, structureInk.axis, 'omit'); const inward = spec?.tickDirection === 'inward'; // A title that only repeats what the labels already say is noise. From a5bc7f4686a8c5795b28d6b7d667ffc8bb1471f8 Mon Sep 17 00:00:00 2001 From: Alper Sarikaya Date: Thu, 30 Jul 2026 16:24:18 -0700 Subject: [PATCH 035/164] fix(site): reset scroll position when navigating between docs The documentation page uses a custom scroll container, so the browser's default scroll-to-top on navigation doesn't apply. Add a useEffect that scrolls the container to the top whenever the active doc slug changes, unless a heading anchor is pending. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- package-lock.json | 10672 ++++++--------------------- site/src/routes/DocSectionPage.tsx | 10 + 2 files changed, 2414 insertions(+), 8268 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0553d4e3..231e2fe7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,204 +1,58 @@ { - "name": "flint-chart-monorepo", + "name": "flint-chart", + "version": "0.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "flint-chart-monorepo", - "hasInstallScript": true, - "workspaces": [ - "packages/flint-js", - "packages/flint-mcp", - "site" - ], + "name": "flint-chart", + "version": "0.1.0", + "license": "MIT", "devDependencies": { - "@types/react": "^18.3.0", - "@types/react-dom": "^18.3.0", - "react": "^18.3.1", - "react-dom": "^18.3.1" + "@types/node": "^20.14.10", + "@typescript-eslint/eslint-plugin": "^8.16.0", + "@typescript-eslint/parser": "^8.16.0", + "eslint": "^9.15.0", + "eslint-plugin-unused-imports": "^4.4.1", + "prettier": "^3.3.0", + "rimraf": "^6.0.1", + "tsup": "^8.3.0", + "typescript": "^5.6.0", + "typescript-eslint": "^8.16.0", + "vitest": "^2.1.0" }, "engines": { "node": ">=18" }, - "optionalDependencies": { - "@rollup/rollup-linux-x64-gnu": "^4.0.0", - "@swc/core-linux-x64-gnu": "1.15.33" - } - }, - "node_modules/@babel/runtime": { - "version": "7.29.7", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@babel/runtime/-/runtime-7.29.7.tgz", - "integrity": "sha1-EgIkUMRaTabY2Ch7GKT/Ldsj92g=", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@codemirror/autocomplete": { - "version": "6.20.3", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@codemirror/autocomplete/-/autocomplete-6.20.3.tgz", - "integrity": "sha1-aWt0AxLGqWLhRWe0mjZhtZJLxa4=", - "license": "MIT", - "dependencies": { - "@codemirror/language": "^6.0.0", - "@codemirror/state": "^6.0.0", - "@codemirror/view": "^6.17.0", - "@lezer/common": "^1.0.0" - } - }, - "node_modules/@codemirror/commands": { - "version": "6.10.4", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@codemirror/commands/-/commands-6.10.4.tgz", - "integrity": "sha1-ZN7BvQQ5dus0TjzJ0VrxO29yS/0=", - "license": "MIT", - "dependencies": { - "@codemirror/language": "^6.0.0", - "@codemirror/state": "^6.7.0", - "@codemirror/view": "^6.27.0", - "@lezer/common": "^1.1.0" - } - }, - "node_modules/@codemirror/lang-json": { - "version": "6.0.2", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@codemirror/lang-json/-/lang-json-6.0.2.tgz", - "integrity": "sha1-BUsWBnEwZmfiXYA4UoYEmEGDYXk=", - "license": "MIT", - "dependencies": { - "@codemirror/language": "^6.0.0", - "@lezer/json": "^1.0.0" - } - }, - "node_modules/@codemirror/language": { - "version": "6.12.4", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@codemirror/language/-/language-6.12.4.tgz", - "integrity": "sha1-AecP1ao6igZ/8d/sddW2OUzfoFg=", - "license": "MIT", - "dependencies": { - "@codemirror/state": "^6.0.0", - "@codemirror/view": "^6.23.0", - "@lezer/common": "^1.5.0", - "@lezer/highlight": "^1.0.0", - "@lezer/lr": "^1.0.0", - "style-mod": "^4.0.0" - } - }, - "node_modules/@codemirror/lint": { - "version": "6.9.7", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@codemirror/lint/-/lint-6.9.7.tgz", - "integrity": "sha1-hB/HM2dDidkf5JocNAJ607q98QU=", - "license": "MIT", - "dependencies": { - "@codemirror/state": "^6.0.0", - "@codemirror/view": "^6.42.0", - "crelt": "^1.0.5" - } - }, - "node_modules/@codemirror/search": { - "version": "6.7.1", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@codemirror/search/-/search-6.7.1.tgz", - "integrity": "sha1-JSOnYocdGK2YLtwtSy+h2kg7A5I=", - "license": "MIT", - "dependencies": { - "@codemirror/state": "^6.0.0", - "@codemirror/view": "^6.37.0", - "crelt": "^1.0.5" - } - }, - "node_modules/@codemirror/state": { - "version": "6.7.1", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@codemirror/state/-/state-6.7.1.tgz", - "integrity": "sha1-noihdEjB28e1Csvu7Jee18zx1vw=", - "license": "MIT", - "dependencies": { - "@marijn/find-cluster-break": "^1.0.0" - } - }, - "node_modules/@codemirror/theme-one-dark": { - "version": "6.1.3", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@codemirror/theme-one-dark/-/theme-one-dark-6.1.3.tgz", - "integrity": "sha1-Hbtz9uc8U8Eq0q7Z9IwmPE5j6jc=", - "license": "MIT", - "dependencies": { - "@codemirror/language": "^6.0.0", - "@codemirror/state": "^6.0.0", - "@codemirror/view": "^6.0.0", - "@lezer/highlight": "^1.0.0" - } - }, - "node_modules/@codemirror/view": { - "version": "6.43.6", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@codemirror/view/-/view-6.43.6.tgz", - "integrity": "sha1-geTusoVefLbGIwX7qN8xm1R2KAw=", - "license": "MIT", - "dependencies": { - "@codemirror/state": "^6.7.0", - "crelt": "^1.0.6", - "style-mod": "^4.1.0", - "w3c-keyname": "^2.2.4" - } - }, - "node_modules/@emnapi/core": { - "version": "1.11.1", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@emnapi/core/-/core-1.11.1.tgz", - "integrity": "sha1-ueEGTzprFjHiQeY460jXNr/TcqY=", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.2.2", - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/core/node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha1-YS7+TtI11Wfoq6Xypfq3AoCt6D8=", - "dev": true, - "license": "0BSD", - "optional": true - }, - "node_modules/@emnapi/runtime": { - "version": "1.11.1", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@emnapi/runtime/-/runtime-1.11.1.tgz", - "integrity": "sha1-WPHz1dgamxL3k6tojJY3GQECfCQ=", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/runtime/node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha1-YS7+TtI11Wfoq6Xypfq3AoCt6D8=", - "dev": true, - "license": "0BSD", - "optional": true - }, - "node_modules/@emnapi/wasi-threads": { - "version": "1.2.2", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", - "integrity": "sha1-TJO+z1v6OxPRu9zAau44MhrYE5o=", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" + "peerDependencies": { + "chart.js": "^4.0.0", + "echarts": "^5.0.0 || ^6.0.0", + "gofish-graphics": "^0.0.22", + "vega": "^5.0.0 || ^6.0.0", + "vega-lite": "^5.0.0 || ^6.0.0" + }, + "peerDependenciesMeta": { + "chart.js": { + "optional": true + }, + "echarts": { + "optional": true + }, + "gofish-graphics": { + "optional": true + }, + "vega": { + "optional": true + }, + "vega-lite": { + "optional": true + } } }, - "node_modules/@emnapi/wasi-threads/node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha1-YS7+TtI11Wfoq6Xypfq3AoCt6D8=", - "dev": true, - "license": "0BSD", - "optional": true - }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.28.1", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", - "integrity": "sha1-egGo0uwvuy2seK2tCbD6eB5Agr4=", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", "cpu": [ "ppc64" ], @@ -213,9 +67,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.28.1", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/android-arm/-/android-arm-0.28.1.tgz", - "integrity": "sha1-cEvSl95tdi3lTqu+r79V9nVqvi8=", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", "cpu": [ "arm" ], @@ -230,9 +84,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.28.1", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", - "integrity": "sha1-tUCifRTkr9BYSWpNvsTT9BTbEQo=", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", "cpu": [ "arm64" ], @@ -247,9 +101,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.28.1", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/android-x64/-/android-x64-0.28.1.tgz", - "integrity": "sha1-0csWbTSw+/D+irRgpVlPJKN4cB4=", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", "cpu": [ "x64" ], @@ -264,9 +118,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.28.1", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", - "integrity": "sha1-EDSyZFf8iGNo/mG70J9lP2r6jlQ=", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", "cpu": [ "arm64" ], @@ -281,9 +135,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.28.1", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", - "integrity": "sha1-ZVVqQyoeTXIDLYIYwZMvzKGkl3I=", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", "cpu": [ "x64" ], @@ -298,9 +152,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.28.1", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", - "integrity": "sha1-LmHgWS+QMNfj2uGO4l68U1kYrvY=", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", "cpu": [ "arm64" ], @@ -315,9 +169,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.28.1", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", - "integrity": "sha1-yV7CiZWe+AecTcqBeh4sS+Zrm9M=", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", "cpu": [ "x64" ], @@ -332,9 +186,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.28.1", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", - "integrity": "sha1-wJoPZ5F1kqwN6JKpvk04FN69Kmw=", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", "cpu": [ "arm" ], @@ -349,9 +203,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.28.1", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", - "integrity": "sha1-QLIhdd2gYYLz7oFBGGxf8wTEpxc=", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", "cpu": [ "arm64" ], @@ -366,9 +220,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.28.1", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", - "integrity": "sha1-pYD5xnZ5eDOJHlGfx6EzfIr9jbM=", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", "cpu": [ "ia32" ], @@ -383,9 +237,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.28.1", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", - "integrity": "sha1-RkUs8yHcf56Rwvp4Cla7Vuec1os=", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", "cpu": [ "loong64" ], @@ -400,9 +254,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.28.1", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", - "integrity": "sha1-QhGzGE3WYI9T3LIuOfXTTuCIUsg=", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", "cpu": [ "mips64el" ], @@ -417,9 +271,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.28.1", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", - "integrity": "sha1-aXhXwqYcubC2u2ZS5AwdxeHKjl0=", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", "cpu": [ "ppc64" ], @@ -434,9 +288,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.28.1", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", - "integrity": "sha1-0ZKUPrFGpArExkl9DPe+NbmGvwg=", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", "cpu": [ "riscv64" ], @@ -451,9 +305,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.28.1", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", - "integrity": "sha1-rOoDVtoODrwI+Xz3ucLkAeHmSNw=", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", "cpu": [ "s390x" ], @@ -468,9 +322,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.28.1", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", - "integrity": "sha1-bww84MtkxTS3DExF7LLBbTTjXf0=", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", "cpu": [ "x64" ], @@ -485,9 +339,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.28.1", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", - "integrity": "sha1-i813B3oNzjN4tXT+2ybSolO3PTY=", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", "cpu": [ "arm64" ], @@ -502,9 +356,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.28.1", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", - "integrity": "sha1-5/sqAemcgwyU5mI82f77TI+1g0c=", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", "cpu": [ "x64" ], @@ -519,9 +373,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.28.1", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", - "integrity": "sha1-xSkJNy24uG4sVeBaiUADO1Zgo7I=", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", "cpu": [ "arm64" ], @@ -536,9 +390,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.28.1", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", - "integrity": "sha1-xCe5vlpkwmL/mn63C1+7qt9EbGw=", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", "cpu": [ "x64" ], @@ -553,9 +407,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.28.1", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", - "integrity": "sha1-3JsUe6yi5sSzyFVxdB70hgpIkJc=", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", "cpu": [ "arm64" ], @@ -570,9 +424,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.28.1", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", - "integrity": "sha1-zoZtEt8TwV5MmfBzo9Rm9uBkmzo=", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", "cpu": [ "x64" ], @@ -587,9 +441,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.28.1", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", - "integrity": "sha1-dGjjaS0B1inVlB5dg4F7uA+eObQ=", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", "cpu": [ "arm64" ], @@ -604,9 +458,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.28.1", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", - "integrity": "sha1-pbwAY/sryrbQ7WPyoVN5WLwmnsY=", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", "cpu": [ "ia32" ], @@ -621,9 +475,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.28.1", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", - "integrity": "sha1-EAZO5E9DR7kMmgK0Rrv4CpFjKxI=", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", "cpu": [ "x64" ], @@ -639,8 +493,8 @@ }, "node_modules/@eslint-community/eslint-utils": { "version": "4.9.1", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", - "integrity": "sha1-TpCvZ7xR3e5s3vUoTt9XLsN2tZU=", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", "dev": true, "license": "MIT", "dependencies": { @@ -658,8 +512,8 @@ }, "node_modules/@eslint-community/regexpp": { "version": "4.12.2", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", - "integrity": "sha1-vM32Fbz3tujbgw7AuNIcmiXeWXs=", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", "dev": true, "license": "MIT", "engines": { @@ -668,8 +522,8 @@ }, "node_modules/@eslint/config-array": { "version": "0.21.2", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@eslint/config-array/-/config-array-0.21.2.tgz", - "integrity": "sha1-8p4iBXrVMWzyODbO6aNMgf/8t+Y=", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -683,15 +537,15 @@ }, "node_modules/@eslint/config-array/node_modules/balanced-match": { "version": "1.0.2", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha1-6D46fj8wCzTLnYf2FfoMvzV2kO4=", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true, "license": "MIT" }, "node_modules/@eslint/config-array/node_modules/brace-expansion": { - "version": "1.1.16", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/brace-expansion/-/brace-expansion-1.1.16.tgz", - "integrity": "sha1-cj06MMBVjCJavJ/Eeac+FOJsPC8=", + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", "dev": true, "license": "MIT", "dependencies": { @@ -701,8 +555,8 @@ }, "node_modules/@eslint/config-array/node_modules/minimatch": { "version": "3.1.5", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha1-WAyI+NVEXyvWqo88re+g3nn71p4=", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -714,8 +568,8 @@ }, "node_modules/@eslint/config-helpers": { "version": "0.4.2", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", - "integrity": "sha1-G9AGzut+LlWyt3OrMY0wDhpmrto=", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -727,8 +581,8 @@ }, "node_modules/@eslint/core": { "version": "0.17.0", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@eslint/core/-/core-0.17.0.tgz", - "integrity": "sha1-dyJYIEE9lhdQnak0IZCiAZ54dhw=", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -739,9 +593,9 @@ } }, "node_modules/@eslint/eslintrc": { - "version": "3.3.6", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@eslint/eslintrc/-/eslintrc-3.3.6.tgz", - "integrity": "sha1-0iv9azp9jh8sCy8ubeERtT7G4T4=", + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", + "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==", "dev": true, "license": "MIT", "dependencies": { @@ -751,7 +605,7 @@ "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", - "js-yaml": "^4.3.0", + "js-yaml": "^4.1.1", "minimatch": "^3.1.5", "strip-json-comments": "^3.1.1" }, @@ -762,34 +616,17 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/@eslint/eslintrc/node_modules/ajv": { - "version": "6.15.0", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/ajv/-/ajv-6.15.0.tgz", - "integrity": "sha1-B+mCx0YmFnqnoklcU4F4ktcTlJI=", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, "node_modules/@eslint/eslintrc/node_modules/balanced-match": { "version": "1.0.2", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha1-6D46fj8wCzTLnYf2FfoMvzV2kO4=", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true, "license": "MIT" }, "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { - "version": "1.1.16", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/brace-expansion/-/brace-expansion-1.1.16.tgz", - "integrity": "sha1-cj06MMBVjCJavJ/Eeac+FOJsPC8=", + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", "dev": true, "license": "MIT", "dependencies": { @@ -799,25 +636,18 @@ }, "node_modules/@eslint/eslintrc/node_modules/ignore": { "version": "5.3.2", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha1-PNQOcp82Q/2HywTlC/DrcivFlvU=", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", "dev": true, "license": "MIT", "engines": { "node": ">= 4" } }, - "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha1-afaofZUTq4u4/mO9sJecRI5oRmA=", - "dev": true, - "license": "MIT" - }, "node_modules/@eslint/eslintrc/node_modules/minimatch": { "version": "3.1.5", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha1-WAyI+NVEXyvWqo88re+g3nn71p4=", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -828,9 +658,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.39.5", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@eslint/js/-/js-9.39.5.tgz", - "integrity": "sha1-by+8/3VQDSKdU14KlJrhNHLIR4c=", + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz", + "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==", "dev": true, "license": "MIT", "engines": { @@ -842,8 +672,8 @@ }, "node_modules/@eslint/object-schema": { "version": "2.1.7", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@eslint/object-schema/-/object-schema-2.1.7.tgz", - "integrity": "sha1-biEmoTR+hqTe34cG7Gf/jhB+u60=", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -852,8 +682,8 @@ }, "node_modules/@eslint/plugin-kit": { "version": "0.4.1", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", - "integrity": "sha1-l3nj/Zt+4zVxpXQ1z0M1oXlKbLI=", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -864,31 +694,10 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@fontsource-variable/inter": { - "version": "5.2.8", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@fontsource-variable/inter/-/inter-5.2.8.tgz", - "integrity": "sha1-KbEUdvUUn2pEO032UW4mAC2HlBo=", - "license": "OFL-1.1", - "funding": { - "url": "https://github.com/sponsors/ayuhito" - } - }, - "node_modules/@hono/node-server": { - "version": "2.0.10", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@hono/node-server/-/node-server-2.0.10.tgz", - "integrity": "sha1-zq0Nl2OasTjC9aOBKJMXY+mSr9U=", - "license": "MIT", - "engines": { - "node": ">=20" - }, - "peerDependencies": { - "hono": "^4" - } - }, "node_modules/@humanfs/core": { "version": "0.19.2", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@humanfs/core/-/core-0.19.2.tgz", - "integrity": "sha1-qCcsoDsqz0kmcCIrIyC2xCG/3mA=", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -900,8 +709,8 @@ }, "node_modules/@humanfs/node": { "version": "0.16.8", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@humanfs/node/-/node-0.16.8.tgz", - "integrity": "sha1-j4AMzME/T4zTEW4tnAqUk52j4+0=", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -915,8 +724,8 @@ }, "node_modules/@humanfs/types": { "version": "0.15.0", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@humanfs/types/-/types-0.15.0.tgz", - "integrity": "sha1-8qCfYgEjkLK/8/xvskjd7IwJoJA=", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", "dev": true, "license": "Apache-2.0", "engines": { @@ -925,8 +734,8 @@ }, "node_modules/@humanwhocodes/module-importer": { "version": "1.0.1", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha1-r1smkaIrRL6EewyoFkHF+2rQFyw=", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -939,8 +748,8 @@ }, "node_modules/@humanwhocodes/retry": { "version": "0.4.3", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@humanwhocodes/retry/-/retry-0.4.3.tgz", - "integrity": "sha1-wrnS43TuYsWG062+qHGZsdenpro=", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", "dev": true, "license": "Apache-2.0", "engines": { @@ -953,8 +762,8 @@ }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha1-Y0Khn0Q0dRjJPkOxrGnes8Rlah8=", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", "dev": true, "license": "MIT", "dependencies": { @@ -964,8 +773,8 @@ }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha1-eg7mAfYPmaIMfHxf8MgDiMEYm9Y=", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", "dev": true, "license": "MIT", "engines": { @@ -974,15 +783,15 @@ }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.5", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha1-aRKwDSxjHA0Vzhp6tXzWV/Ko+Lo=", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", "dev": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.31", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha1-2xXWeByTHzolGj2sOVAcmKYIL9A=", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", "dev": true, "license": "MIT", "dependencies": { @@ -990,619 +799,290 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@kurkle/color": { - "version": "0.3.4", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@kurkle/color/-/color-0.3.4.tgz", - "integrity": "sha1-TU/2d+FgkhT8ccWAEl3d3Yaryr8=", - "license": "MIT" - }, - "node_modules/@lezer/common": { - "version": "1.5.2", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@lezer/common/-/common-1.5.2.tgz", - "integrity": "sha1-1oQNsTd54/G0LnDJqXxAhtEvriI=", - "license": "MIT" - }, - "node_modules/@lezer/highlight": { - "version": "1.2.3", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@lezer/highlight/-/highlight-1.2.3.tgz", - "integrity": "sha1-og8yS3EUii6pum/0Lli7+uxwKFc=", - "license": "MIT", - "dependencies": { - "@lezer/common": "^1.3.0" - } - }, - "node_modules/@lezer/json": { - "version": "1.0.3", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@lezer/json/-/json-1.0.3.tgz", - "integrity": "sha1-53OgEq0AiPvwfOSc+6h1zJ5bwF8=", - "license": "MIT", - "dependencies": { - "@lezer/common": "^1.2.0", - "@lezer/highlight": "^1.0.0", - "@lezer/lr": "^1.0.0" - } - }, - "node_modules/@lezer/lr": { - "version": "1.4.10", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@lezer/lr/-/lr-1.4.10.tgz", - "integrity": "sha1-s6zDblrQSbdN23cZWU5+dNkWH/U=", - "license": "MIT", - "dependencies": { - "@lezer/common": "^1.0.0" - } - }, - "node_modules/@marijn/find-cluster-break": { - "version": "1.0.3", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@marijn/find-cluster-break/-/find-cluster-break-1.0.3.tgz", - "integrity": "sha1-vb6QfgDBfITjP8UbFRnZqiEefo4=", - "license": "MIT" - }, - "node_modules/@modelcontextprotocol/ext-apps": { - "version": "1.7.4", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@modelcontextprotocol/ext-apps/-/ext-apps-1.7.4.tgz", - "integrity": "sha1-i5cAWhNKbakM4Ii/2iCXRkvtm0Y=", - "license": "MIT", - "workspaces": [ - "examples/*" + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.3.tgz", + "integrity": "sha512-x35CNW/ANXG3hE/EZpRU8MXX1JDN86hBb2wMGAtltkz7pc6cxgjpy1OMMfDosOQ+2hWqIkag/fGok1Yady9nGw==", + "cpu": [ + "arm" ], - "dependencies": { - "@standard-schema/spec": "^1.1.0" - }, - "engines": { - "node": ">=20" - }, - "peerDependencies": { - "@modelcontextprotocol/sdk": "^1.29.0", - "react": "^17.0.0 || ^18.0.0 || ^19.0.0", - "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0", - "zod": "^3.25.0 || ^4.0.0" - }, - "peerDependenciesMeta": { - "react": { - "optional": true - }, - "react-dom": { - "optional": true - } - } - }, - "node_modules/@modelcontextprotocol/sdk": { - "version": "1.29.0", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@modelcontextprotocol/sdk/-/sdk-1.29.0.tgz", - "integrity": "sha1-eXhti1JeJp3oUKyCsfH3V/ORX0Q=", + "dev": true, "license": "MIT", - "dependencies": { - "@hono/node-server": "^1.19.9", - "ajv": "^8.17.1", - "ajv-formats": "^3.0.1", - "content-type": "^1.0.5", - "cors": "^2.8.5", - "cross-spawn": "^7.0.5", - "eventsource": "^3.0.2", - "eventsource-parser": "^3.0.0", - "express": "^5.2.1", - "express-rate-limit": "^8.2.1", - "hono": "^4.11.4", - "jose": "^6.1.3", - "json-schema-typed": "^8.0.2", - "pkce-challenge": "^5.0.0", - "raw-body": "^3.0.0", - "zod": "^3.25 || ^4.0", - "zod-to-json-schema": "^3.25.1" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@cfworker/json-schema": "^4.1.1", - "zod": "^3.25 || ^4.0" - }, - "peerDependenciesMeta": { - "@cfworker/json-schema": { - "optional": true - }, - "zod": { - "optional": false - } - } + "optional": true, + "os": [ + "android" + ] }, - "node_modules/@napi-rs/canvas": { - "version": "1.0.2", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@napi-rs/canvas/-/canvas-1.0.2.tgz", - "integrity": "sha1-YkMd86TIdQBuGYl5vrioc8vKDrA=", - "license": "MIT", - "workspaces": [ - "e2e/*" - ], - "engines": { - "node": ">= 10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" - }, - "optionalDependencies": { - "@napi-rs/canvas-android-arm64": "1.0.2", - "@napi-rs/canvas-darwin-arm64": "1.0.2", - "@napi-rs/canvas-darwin-x64": "1.0.2", - "@napi-rs/canvas-linux-arm-gnueabihf": "1.0.2", - "@napi-rs/canvas-linux-arm64-gnu": "1.0.2", - "@napi-rs/canvas-linux-arm64-musl": "1.0.2", - "@napi-rs/canvas-linux-riscv64-gnu": "1.0.2", - "@napi-rs/canvas-linux-x64-gnu": "1.0.2", - "@napi-rs/canvas-linux-x64-musl": "1.0.2", - "@napi-rs/canvas-win32-arm64-msvc": "1.0.2", - "@napi-rs/canvas-win32-x64-msvc": "1.0.2" - } - }, - "node_modules/@napi-rs/canvas-android-arm64": { - "version": "1.0.2", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-1.0.2.tgz", - "integrity": "sha1-jUVdBqjygqSRi/Ez3Qhdupt0YGI=", + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.3.tgz", + "integrity": "sha512-xw3xtkDApIOGayehp2+Rz4zimfkaX65r4t47iy+ymQB2G4iJCBBfj0ogVg5jpvjpn8UWn/+q9tprxleYeNp3Hw==", "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ "android" - ], - "engines": { - "node": ">= 10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" - } + ] }, - "node_modules/@napi-rs/canvas-darwin-arm64": { - "version": "1.0.2", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-1.0.2.tgz", - "integrity": "sha1-n0znqx+bwQVjinQydK8DVxv1bP8=", + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.3.tgz", + "integrity": "sha512-vo6Y5Qfpx7/5EaamIwi0WqW2+zfiusVihKatLvtN1VFVy3D13uERk/6gZLU1UiHRL6fDXqj/ELIeVRGnvcTE1g==", "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ "darwin" - ], - "engines": { - "node": ">= 10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" - } + ] }, - "node_modules/@napi-rs/canvas-darwin-x64": { - "version": "1.0.2", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-1.0.2.tgz", - "integrity": "sha1-X/mTWFb6NAnLAvoXzYUwBeoffYg=", + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.3.tgz", + "integrity": "sha512-D+0QGcZhBzTN82weOnsSlY7V7+RMmPuF1CkbxyMAGE8+ZHeUjyb76ZiWmBlCu//AQQONvxcqRbwZTajZKqjuOw==", "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ "darwin" - ], - "engines": { - "node": ">= 10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" - } + ] }, - "node_modules/@napi-rs/canvas-linux-arm-gnueabihf": { - "version": "1.0.2", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-1.0.2.tgz", - "integrity": "sha1-++4fOk1qJ/G+VTmUh8K/NCucO7Y=", + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.3.tgz", + "integrity": "sha512-6HnvHCT7fDyj6R0Ph7A6x8dQS/S38MClRWeDLqc0MdfWkxjiu1HSDYrdPhqSILzjTIC/pnXbbJbo+ft+gy/9hQ==", "cpu": [ - "arm" + "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" - } + "freebsd" + ] }, - "node_modules/@napi-rs/canvas-linux-arm64-gnu": { - "version": "1.0.2", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-1.0.2.tgz", - "integrity": "sha1-ZDLLhhNwQ38LEgb+Re0MvUyjK/Q=", + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.3.tgz", + "integrity": "sha512-KHLgC3WKlUYW3ShFKnnosZDOJ0xjg9zp7au3sIm2bs/tGBeC2ipmvRh/N7JKi0t9Ue20C0dpEshi8WUubg+cnA==", "cpu": [ - "arm64" + "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" - } + "freebsd" + ] }, - "node_modules/@napi-rs/canvas-linux-arm64-musl": { - "version": "1.0.2", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-1.0.2.tgz", - "integrity": "sha1-ZURXdDYrmQGVZlziwUYFo0LIOIc=", + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.3.tgz", + "integrity": "sha512-DV6fJoxEYWJOvaZIsok7KrYl0tPvga5OZ2yvKHNNYyk/2roMLqQAbGhr78EQ5YhHpnhLKJD3S1WFusAkmUuV5g==", "cpu": [ - "arm64" + "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ "linux" - ], - "engines": { - "node": ">= 10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" - } + ] }, - "node_modules/@napi-rs/canvas-linux-riscv64-gnu": { - "version": "1.0.2", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-1.0.2.tgz", - "integrity": "sha1-ULJ+W7f/2vjauz423K4uuIKz2TQ=", + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.3.tgz", + "integrity": "sha512-mQKoJAzvuOs6F+TZybQO4GOTSMUu7v0WdxEk24krQ/uUxXoPTtHjuaUuPmFhtBcM4K0ons8nrE3JyhTuCFtT/w==", "cpu": [ - "riscv64" + "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ "linux" - ], - "engines": { - "node": ">= 10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" - } + ] }, - "node_modules/@napi-rs/canvas-linux-x64-gnu": { - "version": "1.0.2", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-1.0.2.tgz", - "integrity": "sha1-gOfVc+oDAdSOXeMM2YPb132oiNE=", + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.3.tgz", + "integrity": "sha512-Whjj2qoiJ6+OOJMGptTYazaJvjOJm+iKHpXQM1P3LzGjt7Ff++Tp7nH4N8J/BUA7R9IHfDyx4DJIflifwnbmIA==", "cpu": [ - "x64" + "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ "linux" - ], - "engines": { - "node": ">= 10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" - } + ] }, - "node_modules/@napi-rs/canvas-linux-x64-musl": { - "version": "1.0.2", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-1.0.2.tgz", - "integrity": "sha1-n+OlDOwJMc0vIA2Ke6yR2LDd1aA=", + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.3.tgz", + "integrity": "sha512-4YTNHKqGng5+yiZt3mg77nmyuCfmNfX4fPmyUapBcIk+BdwSwmCWGXOUxhXbBEkFHtoN5boLj/5NON+u5QC9tg==", "cpu": [ - "x64" + "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ "linux" - ], - "engines": { - "node": ">= 10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" - } + ] }, - "node_modules/@napi-rs/canvas-win32-arm64-msvc": { - "version": "1.0.2", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@napi-rs/canvas-win32-arm64-msvc/-/canvas-win32-arm64-msvc-1.0.2.tgz", - "integrity": "sha1-0w4oQfM2Av+ev3L8Aovu7hV398g=", + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.3.tgz", + "integrity": "sha512-SU3kNlhkpI4UqlUc2VXPGK9o886ZsSeGfMAX2ba2b8DKmMXq4AL7KUrkSWVbb7koVqx41Yczx6dx5PNargIrEA==", "cpu": [ - "arm64" + "loong64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" - } + "linux" + ] }, - "node_modules/@napi-rs/canvas-win32-x64-msvc": { - "version": "1.0.2", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-1.0.2.tgz", - "integrity": "sha1-hhul5Ib/GqOB8yXCfOiM+fI3BiY=", + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.3.tgz", + "integrity": "sha512-6lDLl5h4TXpB1mTf2rQWnAk/LcXrx9vBfu/DT5TIPhvMhRWaZ5MxkIc8u4lJAmBo6klTe1ywXIUHFjylW505sg==", "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" + "loong64" ], - "engines": { - "node": ">= 10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" - } - }, - "node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.6", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", - "integrity": "sha1-7TOAbQ+b6Y3HbQw9T9hy/acBtdU=", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@tybys/wasm-util": "^0.10.3" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" - }, - "peerDependencies": { - "@emnapi/core": "^1.7.1", - "@emnapi/runtime": "^1.7.1" - } - }, - "node_modules/@oxc-project/types": { - "version": "0.139.0", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@oxc-project/types/-/types-0.139.0.tgz", - "integrity": "sha1-ONdrnb+TTCoCvhdPsyzuvxgv50I=", "dev": true, "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/Boshen" - } - }, - "node_modules/@resvg/resvg-js": { - "version": "2.6.2", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@resvg/resvg-js/-/resvg-js-2.6.2.tgz", - "integrity": "sha1-PpKpB9iNh5JWxYU0fFshp/O7W0Y=", - "license": "MPL-2.0", - "engines": { - "node": ">= 10" - }, - "optionalDependencies": { - "@resvg/resvg-js-android-arm-eabi": "2.6.2", - "@resvg/resvg-js-android-arm64": "2.6.2", - "@resvg/resvg-js-darwin-arm64": "2.6.2", - "@resvg/resvg-js-darwin-x64": "2.6.2", - "@resvg/resvg-js-linux-arm-gnueabihf": "2.6.2", - "@resvg/resvg-js-linux-arm64-gnu": "2.6.2", - "@resvg/resvg-js-linux-arm64-musl": "2.6.2", - "@resvg/resvg-js-linux-x64-gnu": "2.6.2", - "@resvg/resvg-js-linux-x64-musl": "2.6.2", - "@resvg/resvg-js-win32-arm64-msvc": "2.6.2", - "@resvg/resvg-js-win32-ia32-msvc": "2.6.2", - "@resvg/resvg-js-win32-x64-msvc": "2.6.2" - } - }, - "node_modules/@resvg/resvg-js-android-arm-eabi": { - "version": "2.6.2", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@resvg/resvg-js-android-arm-eabi/-/resvg-js-android-arm-eabi-2.6.2.tgz", - "integrity": "sha1-52HgtogSfbZIefRVF4ySRoqa6r4=", - "cpu": [ - "arm" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@resvg/resvg-js-android-arm64": { - "version": "2.6.2", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@resvg/resvg-js-android-arm64/-/resvg-js-android-arm64-2.6.2.tgz", - "integrity": "sha1-uMtWTX9rPzfZtDEp9dxf4XHiSeQ=", - "cpu": [ - "arm64" - ], - "license": "MPL-2.0", "optional": true, "os": [ - "android" - ], - "engines": { - "node": ">= 10" - } + "linux" + ] }, - "node_modules/@resvg/resvg-js-darwin-arm64": { - "version": "2.6.2", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@resvg/resvg-js-darwin-arm64/-/resvg-js-darwin-arm64-2.6.2.tgz", - "integrity": "sha1-Sb0/rtpcSfUzAtlw5uedAG3hjn0=", + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.3.tgz", + "integrity": "sha512-BMo8bOw8evlup/8G+cj5xWtPyp93xPdyoSN16Zy90Q2QZ0ZYRhCt6ZJSwbrRzG9HApFabjwj2p25TUPDWrhzqQ==", "cpu": [ - "arm64" + "ppc64" ], - "license": "MPL-2.0", + "dev": true, + "license": "MIT", "optional": true, "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } + "linux" + ] }, - "node_modules/@resvg/resvg-js-darwin-x64": { - "version": "2.6.2", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@resvg/resvg-js-darwin-x64/-/resvg-js-darwin-x64-2.6.2.tgz", - "integrity": "sha1-4TRBc6onv7TYgKtXbRrPHBZI+so=", + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.3.tgz", + "integrity": "sha512-E0L8X1dZN1/Rph+5VPF6Xj2G7JJvMACVXtamTJIDrVI44Y3K+G8gQaMEAavbqCGTa16InptiVrX6eM6pmJ+7qA==", "cpu": [ - "x64" + "ppc64" ], - "license": "MPL-2.0", + "dev": true, + "license": "MIT", "optional": true, "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } + "linux" + ] }, - "node_modules/@resvg/resvg-js-linux-arm-gnueabihf": { - "version": "2.6.2", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@resvg/resvg-js-linux-arm-gnueabihf/-/resvg-js-linux-arm-gnueabihf-2.6.2.tgz", - "integrity": "sha1-NMRF66Re/Wj2EwsqtCbXanQkJT0=", + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.3.tgz", + "integrity": "sha512-oZJ/WHaVfHUiRAtmTAeo3DcevNsVvH8mbvodjZy7D5QKvCefO371SiKRpxoDcCxB3PTRTLayWBkvmDQKTcX/sw==", "cpu": [ - "arm" + "riscv64" ], - "license": "MPL-2.0", + "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" - ], - "engines": { - "node": ">= 10" - } + ] }, - "node_modules/@resvg/resvg-js-linux-arm64-gnu": { - "version": "2.6.2", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@resvg/resvg-js-linux-arm64-gnu/-/resvg-js-linux-arm64-gnu-2.6.2.tgz", - "integrity": "sha1-MNpHCH3YFTGCGYuU/p+NmUiQ2uU=", + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.3.tgz", + "integrity": "sha512-Dhbyh7j9FybM3YaTgaHmVALwA8AkUwTPccyCQ79TG9AJUsMQqgN1DDEZNr4+QUfwiWvLDumW5vdwzoeUF+TNxQ==", "cpu": [ - "arm64" + "riscv64" ], - "license": "MPL-2.0", + "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" - ], - "engines": { - "node": ">= 10" - } + ] }, - "node_modules/@resvg/resvg-js-linux-arm64-musl": { - "version": "2.6.2", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@resvg/resvg-js-linux-arm64-musl/-/resvg-js-linux-arm64-musl-2.6.2.tgz", - "integrity": "sha1-XXW4/1yDEDcpwco3eZhzAnU8UNQ=", + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.3.tgz", + "integrity": "sha512-cJd1X5XhHHlltkaypz1UcWLA8AcoIi1aWhsvaWDskD1oz2eKCypnqvTQ8ykMNI0RSmm7NkTdSqSSD7zM0xa6Ig==", "cpu": [ - "arm64" + "s390x" ], - "license": "MPL-2.0", + "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" - ], - "engines": { - "node": ">= 10" - } + ] }, - "node_modules/@resvg/resvg-js-linux-x64-gnu": { - "version": "2.6.2", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@resvg/resvg-js-linux-x64-gnu/-/resvg-js-linux-x64-gnu-2.6.2.tgz", - "integrity": "sha1-QRq+367l7cV8u3cBc2zsulIuJvM=", + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.3.tgz", + "integrity": "sha512-DAZDBHQfG2oQuhY7mc6I3/qB4LU2fQCjRvxbDwd/Jdvb9fypP4IJ4qmtu6lNjes6B531AI8cg1aKC2di97bUxA==", "cpu": [ "x64" ], - "license": "MPL-2.0", + "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" - ], - "engines": { - "node": ">= 10" - } + ] }, - "node_modules/@resvg/resvg-js-linux-x64-musl": { - "version": "2.6.2", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@resvg/resvg-js-linux-x64-musl/-/resvg-js-linux-x64-musl-2.6.2.tgz", - "integrity": "sha1-/kmEA48DcvJ54/9XC3KTTdfrKlw=", + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.3.tgz", + "integrity": "sha512-cRxsE8c13mZOh3vP+wLDxpQBRrOHDIGOWyDL93Sy0Ga8y515fBcC2pjUfFwUe5T7tqvTvWbCpg1URM/AXdWIXA==", "cpu": [ "x64" ], - "license": "MPL-2.0", + "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@resvg/resvg-js-win32-arm64-msvc": { - "version": "2.6.2", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@resvg/resvg-js-win32-arm64-msvc/-/resvg-js-win32-arm64-msvc-2.6.2.tgz", - "integrity": "sha1-06BTz3/2hwh6IQYzDA/arnBiVNE=", - "cpu": [ - "arm64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@resvg/resvg-js-win32-ia32-msvc": { - "version": "2.6.2", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@resvg/resvg-js-win32-ia32-msvc/-/resvg-js-win32-ia32-msvc-2.6.2.tgz", - "integrity": "sha1-fN2hzinvcgnigZHZF/pb7wYkpK0=", - "cpu": [ - "ia32" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } + ] }, - "node_modules/@resvg/resvg-js-win32-x64-msvc": { - "version": "2.6.2", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@resvg/resvg-js-win32-x64-msvc/-/resvg-js-win32-x64-msvc-2.6.2.tgz", - "integrity": "sha1-ywrQRSXWXz3vTI00YVeleXbVs4g=", + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.3.tgz", + "integrity": "sha512-QaWcIgRxqEdQdhJqW4DJctsH6HCmo5vHxY0krHSX4jMtOqfzC+dqDGuHM87bu4H8JBeibWx7jFz+h6/4C8wA5Q==", "cpu": [ "x64" ], - "license": "MPL-2.0", + "dev": true, + "license": "MIT", "optional": true, "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } + "openbsd" + ] }, - "node_modules/@rolldown/binding-android-arm64": { - "version": "1.1.5", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.5.tgz", - "integrity": "sha1-9Yy5oKgSjtBYIoJyBShUf8XANfM=", + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.3.tgz", + "integrity": "sha512-AaXwSvUi3QIPtroAUw1t5yHGIyqKEXwH54WUocFolZhpGDruJcs8c+xPNDRn4XiQsS7MEwnYsHW2l0MBLDMkWg==", "cpu": [ "arm64" ], @@ -1610,16 +1090,13 @@ "license": "MIT", "optional": true, "os": [ - "android" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } + "openharmony" + ] }, - "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.1.5", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.5.tgz", - "integrity": "sha1-RBFEwFpKgxqnUmmrw6SjJDdOpwc=", + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.3.tgz", + "integrity": "sha512-65LAKM/bAWDqKNEelHlcHvm2V+Vfb8C6INFxQXRHCvaVN1rJfwr4NvdP4FyzUaLqWfaCGaadf6UbTm8xJeYfEg==", "cpu": [ "arm64" ], @@ -1627,33 +1104,27 @@ "license": "MIT", "optional": true, "os": [ - "darwin" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } + "win32" + ] }, - "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.1.5", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.5.tgz", - "integrity": "sha1-yC4wZSzvUsSvkl1cZsiVWkAxmBY=", + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.3.tgz", + "integrity": "sha512-EEM2gyhBF5MFnI6vMKdX1LAosE627RGBzIoGMdLloPZkXrUN0Ckqgr2Qi8+J3zip/8NVVro3/FjB+tjhZUgUHA==", "cpu": [ - "x64" + "ia32" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "darwin" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } + "win32" + ] }, - "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.1.5", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.5.tgz", - "integrity": "sha1-wy6c5/ocD7K4CROio6BcPpB9BrA=", + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.3.tgz", + "integrity": "sha512-E5Eb5H/DpxaoXH++Qkv28RcUJboMopmdDUALBczvHMf7hNIxaDZqwY5lK12UK1BHacSmvupoEWGu+n993Z0y1A==", "cpu": [ "x64" ], @@ -1661,7776 +1132,2830 @@ "license": "MIT", "optional": true, "os": [ - "freebsd" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } + "win32" + ] }, - "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.1.5", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.5.tgz", - "integrity": "sha1-zpC14iMWretQLqAQWC9JjNBgTyc=", + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.3.tgz", + "integrity": "sha512-hPt/bgL5cE+Qp+/TPHBqptcAgPzgj46mPcg/16zNUmbQk0j+mOEQV/+Lqu8QRtDV3Ek95Q6FeFITpuhl6OTsAA==", "cpu": [ - "arm" + "x64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } + "win32" + ] }, - "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.1.5", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.5.tgz", - "integrity": "sha1-kZRxEMTdqk7vsATlJogHCXcIUgE=", - "cpu": [ - "arm64" - ], + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } + "license": "MIT" }, - "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.1.5", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.5.tgz", - "integrity": "sha1-6zKy1BCMHHArkejN6KBD6uXKqU0=", - "cpu": [ - "arm64" - ], + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.19.41", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.41.tgz", + "integrity": "sha512-ECymXOukMnOoVkC2bb1Vc/w/836DXncOg5m8Xj1RH7xSHZJWNYY6Zh7EH477vcnD5egKNNfy2RpNOmuChhFPgQ==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" + "dependencies": { + "undici-types": "~6.21.0" } }, - "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.1.5", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.5.tgz", - "integrity": "sha1-zLlDwR5acmVcuwL8EWNUHOtkB4I=", - "cpu": [ - "ppc64" - ], + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.3.tgz", + "integrity": "sha512-PwFvSKsXGShKGW6n5bZOhGHEcCZXM8HofLK9fNsEwZXzFRjoY+XT1Vsf1zgyXdwTr0ZYz1/2tkZ0DBTT9jZjhw==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.59.3", + "@typescript-eslint/type-utils": "8.59.3", + "@typescript-eslint/utils": "8.59.3", + "@typescript-eslint/visitor-keys": "8.59.3", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.59.3", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.1.5", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.5.tgz", - "integrity": "sha1-ozcx7lZ+kLdfrG5eVTB+iiswOPA=", - "cpu": [ - "s390x" - ], + "node_modules/@typescript-eslint/parser": { + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.59.3.tgz", + "integrity": "sha512-HPwA+hVkfcriajbNvTmZv4VRauibay+cWArYUYq7u7W7PmGShMxbPxLvrwDme55a6d5alG3nrYfhyJ/G28XlLg==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "@typescript-eslint/scope-manager": "8.59.3", + "@typescript-eslint/types": "8.59.3", + "@typescript-eslint/typescript-estree": "8.59.3", + "@typescript-eslint/visitor-keys": "8.59.3", + "debug": "^4.4.3" + }, "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.1.5", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.5.tgz", - "integrity": "sha1-p0wBqqzt/BHDm2/rozpfoMZUlJ8=", - "cpu": [ - "x64" - ], + "node_modules/@typescript-eslint/project-service": { + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.59.3.tgz", + "integrity": "sha512-ECiUWa/KYRGDFUqTNehaRgzDshnJfkTABJxVemHk4ko22gcr0ukloKjWvyQ64g8YCV/UI47kN1dbmjf/GaQYng==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.59.3", + "@typescript-eslint/types": "^8.59.3", + "debug": "^4.4.3" + }, "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.1.5", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.5.tgz", - "integrity": "sha1-KM0XhJT6HmXbpBIim1zlXE3Vy9E=", - "cpu": [ - "x64" - ], + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.3.tgz", + "integrity": "sha512-t2LvZnoEfzKtnPjgeEu41xw5gxq9mQVfYy4OoZ4Vlt0sk3JwxmhCca/AR7DwOiHrjWgjAj6as4AhRLKSDfvZIA==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "@typescript-eslint/types": "8.59.3", + "@typescript-eslint/visitor-keys": "8.59.3" + }, "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.1.5", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.5.tgz", - "integrity": "sha1-98dfqRP8IIhNJqfUiNT1xZfNccA=", - "cpu": [ - "arm64" - ], + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.3.tgz", + "integrity": "sha512-PcIJHjmaREXLgIAIzLnSY9VucEzz8FKXsRgFa1DmdGCK/5tJpW03TKJF01Q6VZd1lLdz2sIKPWaDUZN9dp//dw==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/@rolldown/binding-wasm32-wasi": { - "version": "1.1.5", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.5.tgz", - "integrity": "sha1-w3lYGUd4cIHfNj6hBhQPzV/sJS0=", - "cpu": [ - "wasm32" - ], + "node_modules/@typescript-eslint/type-utils": { + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.59.3.tgz", + "integrity": "sha512-g71d8QD8UaiHGvrJwyIS1hCX5r63w6Jll+4VEYhEAHXTDIqX1JgxhTAbEHtKntL9kuc4jRo7/GWw5xfCepSccQ==", "dev": true, "license": "MIT", - "optional": true, "dependencies": { - "@emnapi/core": "1.11.1", - "@emnapi/runtime": "1.11.1", - "@napi-rs/wasm-runtime": "^1.1.6" + "@typescript-eslint/types": "8.59.3", + "@typescript-eslint/typescript-estree": "8.59.3", + "@typescript-eslint/utils": "8.59.3", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" }, "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.1.5", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.5.tgz", - "integrity": "sha1-8jyIaU96cpoS85UCSu4434Cha6M=", - "cpu": [ - "arm64" - ], + "node_modules/@typescript-eslint/types": { + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.3.tgz", + "integrity": "sha512-ePFoH0g4ludssdRFqqDxQePCxU4WQyRa9+XVwjm7yLn0FKhMeoetC+qBEEI1Eyb1pGSDveTIT09Bvw2WhlGayg==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "win32" - ], "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.1.5", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.5.tgz", - "integrity": "sha1-M3fI3g5WqIV/ESF1YRRwkOh0y0Y=", - "cpu": [ - "x64" - ], + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.3.tgz", + "integrity": "sha512-CbRjVRAf7Lr9Kr8RopKcbY45p2VfmmHrm0ygOCYFi7oU8q19m0Fs/6iHS7kNOmwpp+ob07ZVcAqlxUod9lYdmg==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "win32" - ], + "dependencies": { + "@typescript-eslint/project-service": "8.59.3", + "@typescript-eslint/tsconfig-utils": "8.59.3", + "@typescript-eslint/types": "8.59.3", + "@typescript-eslint/visitor-keys": "8.59.3", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/@rolldown/pluginutils": { - "version": "1.0.1", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", - "integrity": "sha1-4/zuCT+7XOdl4a0Ij/TeKIn2+b4=", + "node_modules/@typescript-eslint/utils": { + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.59.3.tgz", + "integrity": "sha512-JAvT14goBzRzzzZyqq3P9BLArIxTtQURUtFgQ/V7FO+eU+Gg6ES+5ymOPP1wRxXcxAYeivCk4uS3jCKWI1K8Zg==", "dev": true, - "license": "MIT" + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.59.3", + "@typescript-eslint/types": "8.59.3", + "@typescript-eslint/typescript-estree": "8.59.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.62.2", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", - "integrity": "sha1-XphJtmHCIpz5Z6CNvi276ejJkeU=", - "cpu": [ - "arm" - ], + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.3.tgz", + "integrity": "sha512-f1UQF7ggd42YiwI5wGrRaPsa+P0CINBlrkLPmGfpq/u/I/oVtecoEIfFR9ag/oa1sLOsRNZ6xehf6qMZhQGBDg==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "android" - ] + "dependencies": { + "@typescript-eslint/types": "8.59.3", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.62.2", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", - "integrity": "sha1-WwaZ7l3UhLIiye10r/Q8keqLF/g=", - "cpu": [ - "arm64" - ], + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.62.2", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", - "integrity": "sha1-i8UsnXo86NBTPDUanJNd54HaoG8=", - "cpu": [ - "arm64" - ], + "node_modules/@vitest/expect": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.9.tgz", + "integrity": "sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] + "dependencies": { + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.62.2", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz", - "integrity": "sha1-ui7z6PsxDwrzVYjycM+lqpbkh2Q=", - "cpu": [ - "x64" - ], + "node_modules/@vitest/mocker": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.9.tgz", + "integrity": "sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] + "dependencies": { + "@vitest/spy": "2.1.9", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.12" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.62.2", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz", - "integrity": "sha1-k7EL2/6K2iJri8DALva39URHTZY=", - "cpu": [ - "arm64" - ], + "node_modules/@vitest/pretty-format": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.9.tgz", + "integrity": "sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] + "dependencies": { + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.62.2", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz", - "integrity": "sha1-PoqjjvPJwwCUaHHj/bsMMOCiD4Y=", - "cpu": [ - "x64" - ], + "node_modules/@vitest/runner": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.9.tgz", + "integrity": "sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] + "dependencies": { + "@vitest/utils": "2.1.9", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.62.2", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz", - "integrity": "sha1-HXmUOEuwrRvEGSG1BuFkLU+df8M=", - "cpu": [ - "arm" - ], + "node_modules/@vitest/runner/node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "license": "MIT" }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.62.2", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz", - "integrity": "sha1-plQPR8+ESla4DKn/ldKs37LO+Xs=", - "cpu": [ - "arm" - ], + "node_modules/@vitest/snapshot": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.9.tgz", + "integrity": "sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "magic-string": "^0.30.12", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.62.2", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", - "integrity": "sha1-QE8gRWUYQMv0jakbptD0kPC8LL8=", - "cpu": [ - "arm64" - ], + "node_modules/@vitest/snapshot/node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "license": "MIT" }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.62.2", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz", - "integrity": "sha1-o0BP/d97R0tIyZuciTtiR7t2W6U=", - "cpu": [ - "arm64" - ], + "node_modules/@vitest/spy": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.9.tgz", + "integrity": "sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "dependencies": { + "tinyspy": "^3.0.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } }, - "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.62.2", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", - "integrity": "sha1-6KrG1Umzd5ReNJiC8Zm3yOt1yjg=", - "cpu": [ - "loong64" - ], + "node_modules/@vitest/utils": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.9.tgz", + "integrity": "sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "loupe": "^3.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } }, - "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.62.2", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", - "integrity": "sha1-bi5E6lAxCzpYIHipFeX+uHnIINQ=", - "cpu": [ - "loong64" - ], + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } }, - "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.62.2", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", - "integrity": "sha1-aJgwLabXegU3zeZLK0xrYGWb0RA=", - "cpu": [ - "ppc64" - ], + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } }, - "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.62.2", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz", - "integrity": "sha1-MzcXyV3Vpmvvj2Pn74qf2EX9GNA=", - "cpu": [ - "ppc64" - ], + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.62.2", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz", - "integrity": "sha1-gbwGujgDUgBNAfSCbrfNzO+gW60=", - "cpu": [ - "riscv64" - ], + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.62.2", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz", - "integrity": "sha1-lafNOd4hOJrWeIpShOqqc44pykw=", - "cpu": [ - "riscv64" - ], + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": ">=12" + } }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.62.2", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz", - "integrity": "sha1-BubbLsG8SLU3THkj74PC6wJLJFI=", - "cpu": [ - "s390x" - ], + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": "18 || 20 || >=22" + } }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.62.2", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", - "integrity": "sha1-XcgYmIKF4J6IeQxkYt73JBPfLaM=", - "cpu": [ - "x64" - ], + "node_modules/brace-expansion": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.62.2", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", - "integrity": "sha1-IID0qTNJ6a/TS+b8GjfgH8i/yA8=", - "cpu": [ - "x64" - ], + "node_modules/bundle-require": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/bundle-require/-/bundle-require-5.1.0.tgz", + "integrity": "sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "dependencies": { + "load-tsconfig": "^0.2.3" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "peerDependencies": { + "esbuild": ">=0.18" + } }, - "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.62.2", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz", - "integrity": "sha1-IdZKistmIhckuSPlGvUzPfGvBEs=", - "cpu": [ - "x64" - ], + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ] + "engines": { + "node": ">=8" + } }, - "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.62.2", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz", - "integrity": "sha1-jg/NnQIUHjN7TFtc/1dsuadrG6A=", - "cpu": [ - "arm64" - ], + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ] + "engines": { + "node": ">=6" + } }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.62.2", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz", - "integrity": "sha1-vbTMTv1Y7+gIIDNH8PVGPw6hblI=", - "cpu": [ - "arm64" - ], + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "win32" - ] + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.62.2", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz", - "integrity": "sha1-26695a/STq4O7+kV2QFjLny1mGA=", - "cpu": [ - "ia32" - ], + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "win32" - ] + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } }, - "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.62.2", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", - "integrity": "sha1-hBCehf6l+PE1NJn5ZXj9wqDosTg=", - "cpu": [ - "x64" - ], + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "win32" - ] + "engines": { + "node": ">= 16" + } }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.62.2", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", - "integrity": "sha1-NnHOP5uSjVwB+Hl5LVwLYK4U1K0=", - "cpu": [ - "x64" - ], + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@standard-schema/spec": { - "version": "1.1.0", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@standard-schema/spec/-/spec-1.1.0.tgz", - "integrity": "sha1-p5tV26+GBIEvUtFAssmrQbwVC7g=", - "license": "MIT" - }, - "node_modules/@swc/core": { - "version": "1.15.43", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@swc/core/-/core-1.15.43.tgz", - "integrity": "sha1-ZT5lc5aP1cdBY7mIXqCpMwEsnyI=", - "dev": true, - "hasInstallScript": true, - "license": "Apache-2.0", "dependencies": { - "@swc/counter": "^0.1.3", - "@swc/types": "^0.1.27" + "readdirp": "^4.0.1" }, "engines": { - "node": ">=10" + "node": ">= 14.16.0" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/swc" - }, - "optionalDependencies": { - "@swc/core-darwin-arm64": "1.15.43", - "@swc/core-darwin-x64": "1.15.43", - "@swc/core-linux-arm-gnueabihf": "1.15.43", - "@swc/core-linux-arm64-gnu": "1.15.43", - "@swc/core-linux-arm64-musl": "1.15.43", - "@swc/core-linux-ppc64-gnu": "1.15.43", - "@swc/core-linux-s390x-gnu": "1.15.43", - "@swc/core-linux-x64-gnu": "1.15.43", - "@swc/core-linux-x64-musl": "1.15.43", - "@swc/core-win32-arm64-msvc": "1.15.43", - "@swc/core-win32-ia32-msvc": "1.15.43", - "@swc/core-win32-x64-msvc": "1.15.43" - }, - "peerDependencies": { - "@swc/helpers": ">=0.5.17" - }, - "peerDependenciesMeta": { - "@swc/helpers": { - "optional": true - } + "url": "https://paulmillr.com/funding/" } }, - "node_modules/@swc/core-darwin-arm64": { - "version": "1.15.43", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@swc/core-darwin-arm64/-/core-darwin-arm64-1.15.43.tgz", - "integrity": "sha1-OGKU+EJ93i3xpw3QpYJtZ69w6ZY=", - "cpu": [ - "arm64" - ], + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, - "license": "Apache-2.0 AND MIT", - "optional": true, - "os": [ - "darwin" - ], + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, "engines": { - "node": ">=10" + "node": ">=7.0.0" } }, - "node_modules/@swc/core-darwin-x64": { - "version": "1.15.43", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@swc/core-darwin-x64/-/core-darwin-x64-1.15.43.tgz", - "integrity": "sha1-xII1KcQk4q4lt+t4ZDhHR0FSH8s=", - "cpu": [ - "x64" - ], + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true, - "license": "Apache-2.0 AND MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=10" - } + "license": "MIT" }, - "node_modules/@swc/core-linux-arm-gnueabihf": { - "version": "1.15.43", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.15.43.tgz", - "integrity": "sha1-wKDtF8/8XUrxkpNWZ/EvBf7rOfk=", - "cpu": [ - "arm" - ], + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], + "license": "MIT", "engines": { - "node": ">=10" + "node": ">= 6" } }, - "node_modules/@swc/core-linux-arm64-gnu": { - "version": "1.15.43", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.15.43.tgz", - "integrity": "sha1-HrLZxe7uW7nQBZm0dd3DHcKHDSI=", - "cpu": [ - "arm64" - ], + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true, - "license": "Apache-2.0 AND MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=10" - } + "license": "MIT" }, - "node_modules/@swc/core-linux-arm64-musl": { - "version": "1.15.43", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.15.43.tgz", - "integrity": "sha1-6mtcOAiPOSGleSLTkxstdP0jqf0=", - "cpu": [ - "arm64" - ], + "node_modules/confbox": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", "dev": true, - "license": "Apache-2.0 AND MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=10" - } + "license": "MIT" }, - "node_modules/@swc/core-linux-ppc64-gnu": { - "version": "1.15.43", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@swc/core-linux-ppc64-gnu/-/core-linux-ppc64-gnu-1.15.43.tgz", - "integrity": "sha1-U4+sMLvV8eZ4u3usnMxiJGpvbXo=", - "cpu": [ - "ppc64" - ], + "node_modules/consola": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", "dev": true, - "license": "Apache-2.0 AND MIT", - "optional": true, - "os": [ - "linux" - ], + "license": "MIT", "engines": { - "node": ">=10" + "node": "^14.18.0 || >=16.10.0" } }, - "node_modules/@swc/core-linux-s390x-gnu": { - "version": "1.15.43", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@swc/core-linux-s390x-gnu/-/core-linux-s390x-gnu-1.15.43.tgz", - "integrity": "sha1-7lZLRfP1eLH8ghNsTasWMYkxZkE=", - "cpu": [ - "s390x" - ], + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, - "license": "Apache-2.0 AND MIT", - "optional": true, - "os": [ - "linux" - ], + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, "engines": { - "node": ">=10" + "node": ">= 8" } }, - "node_modules/@swc/core-linux-x64-gnu": { - "version": "1.15.33", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.15.33.tgz", - "integrity": "sha1-SfNlWO3gcucZmao38SM2fa7SpmI=", - "cpu": [ - "x64" - ], - "license": "Apache-2.0 AND MIT", - "optional": true, - "os": [ - "linux" - ], + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, "engines": { - "node": ">=10" + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } } }, - "node_modules/@swc/core-linux-x64-musl": { - "version": "1.15.43", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.15.43.tgz", - "integrity": "sha1-U59vJyHAzDLl21zw1FPIIEX2Zi0=", - "cpu": [ - "x64" - ], + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", "dev": true, - "license": "Apache-2.0 AND MIT", - "optional": true, - "os": [ - "linux" - ], + "license": "MIT", "engines": { - "node": ">=10" + "node": ">=6" } }, - "node_modules/@swc/core-win32-arm64-msvc": { - "version": "1.15.43", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.15.43.tgz", - "integrity": "sha1-t7trYR1ISsGdDuIUaecBLWRsKLU=", - "cpu": [ - "arm64" - ], + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true, - "license": "Apache-2.0 AND MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=10" - } + "license": "MIT" }, - "node_modules/@swc/core-win32-ia32-msvc": { - "version": "1.15.43", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.15.43.tgz", - "integrity": "sha1-5bJXIqfSe7DJqb3ueGPynIZ0Nk4=", - "cpu": [ - "ia32" - ], + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", "dev": true, - "license": "Apache-2.0 AND MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=10" - } + "license": "MIT" }, - "node_modules/@swc/core-win32-x64-msvc": { - "version": "1.15.43", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.15.43.tgz", - "integrity": "sha1-0ohCYhIBw0U4PUaNQMCWSLbNbmg=", - "cpu": [ - "x64" - ], + "node_modules/esbuild": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", "dev": true, - "license": "Apache-2.0 AND MIT", - "optional": true, - "os": [ - "win32" - ], + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, "engines": { - "node": ">=10" + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" } }, - "node_modules/@swc/core/node_modules/@swc/core-linux-x64-gnu": { - "version": "1.15.43", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.15.43.tgz", - "integrity": "sha1-5uO/6naSHH9eFtUKEmYV8uBM4cg=", - "cpu": [ - "x64" - ], + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "dev": true, - "license": "Apache-2.0 AND MIT", - "optional": true, - "os": [ - "linux" - ], + "license": "MIT", "engines": { "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@swc/counter": { - "version": "0.1.3", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@swc/counter/-/counter-0.1.3.tgz", - "integrity": "sha1-zHRjvQKUlhHGMpWW/M0rDseCsOk=", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/@swc/types": { - "version": "0.1.27", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@swc/types/-/types-0.1.27.tgz", - "integrity": "sha1-EggLDEJt6kUGNPIC2aPIKsOW55M=", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@swc/counter": "^0.1.3" - } - }, - "node_modules/@tybys/wasm-util": { - "version": "0.10.3", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", - "integrity": "sha1-AVy6np3UfOFNA9KoxdVHv7FpZl0=", + "node_modules/eslint": { + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", + "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", "dev": true, "license": "MIT", - "optional": true, "dependencies": { - "tslib": "^2.4.0" + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.2", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.5", + "@eslint/js": "9.39.4", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } } }, - "node_modules/@tybys/wasm-util/node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha1-YS7+TtI11Wfoq6Xypfq3AoCt6D8=", - "dev": true, - "license": "0BSD", - "optional": true - }, - "node_modules/@types/chai": { - "version": "5.2.3", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@types/chai/-/chai-5.2.3.tgz", - "integrity": "sha1-jpzZ4cNYH6azQaWu1ViOsoW+C0o=", + "node_modules/eslint-plugin-unused-imports": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-unused-imports/-/eslint-plugin-unused-imports-4.4.1.tgz", + "integrity": "sha512-oZGYUz1X3sRMGUB+0cZyK2VcvRX5lm/vB56PgNNcU+7ficUCKm66oZWKUubXWnOuPjQ8PvmXtCViXBMONPe7tQ==", "dev": true, "license": "MIT", - "dependencies": { - "@types/deep-eql": "*", - "assertion-error": "^2.0.1" - } - }, - "node_modules/@types/debug": { - "version": "4.1.13", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@types/debug/-/debug-4.1.13.tgz", - "integrity": "sha1-ItHMnVQtNZPK6nZPl0MGqzYobuc=", - "license": "MIT", - "dependencies": { - "@types/ms": "*" + "peerDependencies": { + "@typescript-eslint/eslint-plugin": "^8.0.0-0 || ^7.0.0 || ^6.0.0 || ^5.0.0", + "eslint": "^10.0.0 || ^9.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@typescript-eslint/eslint-plugin": { + "optional": true + } } }, - "node_modules/@types/deep-eql": { - "version": "4.0.2", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@types/deep-eql/-/deep-eql-4.0.2.tgz", - "integrity": "sha1-M0MRlx06BxIefrkbaEpgXn7qnL0=", + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", "dev": true, - "license": "MIT" - }, - "node_modules/@types/estree": { - "version": "1.0.9", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@types/estree/-/estree-1.0.9.tgz", - "integrity": "sha1-zz8Oh2177hWpOrkluCv1cKOQSiQ=", - "license": "MIT" - }, - "node_modules/@types/estree-jsx": { - "version": "1.0.5", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@types/estree-jsx/-/estree-jsx-1.0.5.tgz", - "integrity": "sha1-hYqI6iDzT+ZREfAFpon6Hr9w3Bg=", - "license": "MIT", - "dependencies": { - "@types/estree": "*" - } - }, - "node_modules/@types/geojson": { - "version": "7946.0.16", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@types/geojson/-/geojson-7946.0.16.tgz", - "integrity": "sha1-jr5T1p762nBERU4zBcGQF9l87So=", - "license": "MIT" - }, - "node_modules/@types/hast": { - "version": "3.0.5", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@types/hast/-/hast-3.0.5.tgz", - "integrity": "sha1-SAIN5MDmNJL0yp20IGjBCPaLf48=", - "license": "MIT", + "license": "BSD-2-Clause", "dependencies": { - "@types/unist": "*" + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha1-WWoXRyM2lNUPatinhp/Lb1bPWEE=", + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", "dev": true, - "license": "MIT" - }, - "node_modules/@types/katex": { - "version": "0.16.8", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@types/katex/-/katex-0.16.8.tgz", - "integrity": "sha1-gL8+CBTQmoRkEqCw8UCUa3nDbD4=", - "license": "MIT" - }, - "node_modules/@types/mdast": { - "version": "4.0.4", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@types/mdast/-/mdast-4.0.4.tgz", - "integrity": "sha1-fM9y7dLxqn3TQ34YDGQ3NYWATdY=", - "license": "MIT", - "dependencies": { - "@types/unist": "*" + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, - "node_modules/@types/ms": { - "version": "2.1.0", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@types/ms/-/ms-2.1.0.tgz", - "integrity": "sha1-BSqmekjszEMJ1/AZG35BQ0uQu3g=", - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "20.19.43", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@types/node/-/node-20.19.43.tgz", - "integrity": "sha1-/Oz1gLpCoNtVz0BMNyyXlzw3bJc=", + "node_modules/eslint/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.21.0" - } - }, - "node_modules/@types/prismjs": { - "version": "1.26.6", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@types/prismjs/-/prismjs-1.26.6.tgz", - "integrity": "sha1-bqJ8Em1kUxmuT3BV7aY6noNcAYc=", - "license": "MIT" - }, - "node_modules/@types/prop-types": { - "version": "15.7.15", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@types/prop-types/-/prop-types-15.7.15.tgz", - "integrity": "sha1-5uWobWAr6spxzlFj+t9fldcJMcc=", "license": "MIT" }, - "node_modules/@types/react": { - "version": "18.3.31", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@types/react/-/react-18.3.31.tgz", - "integrity": "sha1-teleKP/M6rjZgvM/LrB24XZTwqQ=", - "license": "MIT", - "dependencies": { - "@types/prop-types": "*", - "csstype": "^3.2.2" - } - }, - "node_modules/@types/react-dom": { - "version": "18.3.7", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@types/react-dom/-/react-dom-18.3.7.tgz", - "integrity": "sha1-uJ3fLNg7T+r8xOLqQa/fuVoNGU8=", - "dev": true, - "license": "MIT", - "peerDependencies": { - "@types/react": "^18.0.0" - } - }, - "node_modules/@types/react-syntax-highlighter": { - "version": "15.5.13", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@types/react-syntax-highlighter/-/react-syntax-highlighter-15.5.13.tgz", - "integrity": "sha1-xbr2KjIZs78o05z+pV0KSaJj0fI=", + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", "dev": true, "license": "MIT", "dependencies": { - "@types/react": "*" + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" } }, - "node_modules/@types/unist": { - "version": "3.0.3", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@types/unist/-/unist-3.0.3.tgz", - "integrity": "sha1-rKqw+RnOaczmKcLU7S60rcG2wgw=", - "license": "MIT" - }, - "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.64.0", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.64.0.tgz", - "integrity": "sha1-caDD1fil5sXf208PBL0b+1ctXiQ=", + "node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.64.0", - "@typescript-eslint/type-utils": "8.64.0", - "@typescript-eslint/utils": "8.64.0", - "@typescript-eslint/visitor-keys": "8.64.0", - "ignore": "^7.0.5", - "natural-compare": "^1.4.0", - "ts-api-utils": "^2.5.0" - }, + "license": "Apache-2.0", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "@typescript-eslint/parser": "^8.64.0", - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" + "url": "https://opencollective.com/eslint" } }, - "node_modules/@typescript-eslint/parser": { - "version": "8.64.0", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@typescript-eslint/parser/-/parser-8.64.0.tgz", - "integrity": "sha1-yYZKHMKKE/8ppzFPve8FKLsSL3I=", + "node_modules/eslint/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", "dev": true, "license": "MIT", - "dependencies": { - "@typescript-eslint/scope-manager": "8.64.0", - "@typescript-eslint/types": "8.64.0", - "@typescript-eslint/typescript-estree": "8.64.0", - "@typescript-eslint/visitor-keys": "8.64.0", - "debug": "^4.4.3" - }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" + "node": ">= 4" } }, - "node_modules/@typescript-eslint/project-service": { - "version": "8.64.0", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@typescript-eslint/project-service/-/project-service-8.64.0.tgz", - "integrity": "sha1-FMTik5DXMlp/ihIYwniP1km4XaY=", + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, - "license": "MIT", + "license": "ISC", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.64.0", - "@typescript-eslint/types": "^8.64.0", - "debug": "^4.4.3" + "brace-expansion": "^1.1.7" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.1.0" + "node": "*" } }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "8.64.0", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@typescript-eslint/scope-manager/-/scope-manager-8.64.0.tgz", - "integrity": "sha1-1F8VMEqUyFw52zF7cXsVj7YlmVg=", + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", "dev": true, - "license": "MIT", + "license": "BSD-2-Clause", "dependencies": { - "@typescript-eslint/types": "8.64.0", - "@typescript-eslint/visitor-keys": "8.64.0" + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "url": "https://opencollective.com/eslint" } }, - "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.64.0", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.64.0.tgz", - "integrity": "sha1-xirI6pFzw8rIs4uOZuMKBGtUiFE=", + "node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", "dev": true, - "license": "MIT", + "license": "Apache-2.0", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.1.0" + "url": "https://opencollective.com/eslint" } }, - "node_modules/@typescript-eslint/type-utils": { - "version": "8.64.0", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@typescript-eslint/type-utils/-/type-utils-8.64.0.tgz", - "integrity": "sha1-EG+n1Yz5z3dY892OQmrII37OrPM=", + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", "dev": true, - "license": "MIT", + "license": "BSD-3-Clause", "dependencies": { - "@typescript-eslint/types": "8.64.0", - "@typescript-eslint/typescript-estree": "8.64.0", - "@typescript-eslint/utils": "8.64.0", - "debug": "^4.4.3", - "ts-api-utils": "^2.5.0" + "estraverse": "^5.1.0" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" + "engines": { + "node": ">=4.0" } }, - "node_modules/@typescript-eslint/types": { - "version": "8.64.0", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@typescript-eslint/types/-/types-8.64.0.tgz", - "integrity": "sha1-tB+O9d1AYWkIZYuZEZep1IbNpgs=", + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true, - "license": "MIT", + "license": "BSD-2-Clause", "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "node": ">=4.0" } }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.64.0", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@typescript-eslint/typescript-estree/-/typescript-estree-8.64.0.tgz", - "integrity": "sha1-uNUSVeLXJutL2A05ek+0FwwC7sw=", + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.64.0", - "@typescript-eslint/tsconfig-utils": "8.64.0", - "@typescript-eslint/types": "8.64.0", - "@typescript-eslint/visitor-keys": "8.64.0", - "debug": "^4.4.3", - "minimatch": "^10.2.2", - "semver": "^7.7.3", - "tinyglobby": "^0.2.15", - "ts-api-utils": "^2.5.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.1.0" + "@types/estree": "^1.0.0" } }, - "node_modules/@typescript-eslint/utils": { - "version": "8.64.0", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@typescript-eslint/utils/-/utils-8.64.0.tgz", - "integrity": "sha1-mLsgEM+3VLQZhbnJPm6LPc171gA=", + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.64.0", - "@typescript-eslint/types": "8.64.0", - "@typescript-eslint/typescript-estree": "8.64.0" - }, + "license": "BSD-2-Clause", "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" + "node": ">=0.10.0" } }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.64.0", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@typescript-eslint/visitor-keys/-/visitor-keys-8.64.0.tgz", - "integrity": "sha1-eghCHRDlSWBzM1LNfJX6sXhOhHM=", + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.64.0", - "eslint-visitor-keys": "^5.0.0" - }, + "license": "Apache-2.0", "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "node": ">=12.0.0" } }, - "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { - "version": "5.0.1", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", - "integrity": "sha1-njyUiWl4JNLUzjqK0SYo+R6fWb4=", + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } + "license": "MIT" }, - "node_modules/@uiw/codemirror-extensions-basic-setup": { - "version": "4.25.11", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@uiw/codemirror-extensions-basic-setup/-/codemirror-extensions-basic-setup-4.25.11.tgz", - "integrity": "sha1-pF4GBOtqKf/6FStoTQx3Asbmm98=", - "license": "MIT", - "dependencies": { - "@codemirror/autocomplete": "^6.0.0", - "@codemirror/commands": "^6.0.0", - "@codemirror/language": "^6.0.0", - "@codemirror/lint": "^6.0.0", - "@codemirror/search": "^6.0.0", - "@codemirror/state": "^6.0.0", - "@codemirror/view": "^6.0.0" - }, - "funding": { - "url": "https://jaywcjlove.github.io/#/sponsor" - }, - "peerDependencies": { - "@codemirror/autocomplete": ">=6.0.0", - "@codemirror/commands": ">=6.0.0", - "@codemirror/language": ">=6.0.0", - "@codemirror/lint": ">=6.0.0", - "@codemirror/search": ">=6.0.0", - "@codemirror/state": ">=6.0.0", - "@codemirror/view": ">=6.0.0" - } + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" }, - "node_modules/@uiw/react-codemirror": { - "version": "4.25.11", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@uiw/react-codemirror/-/react-codemirror-4.25.11.tgz", - "integrity": "sha1-AfFUrM36PfZ03C4AQQZ/UU30j8I=", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.18.6", - "@codemirror/commands": "^6.1.0", - "@codemirror/state": "^6.1.1", - "@codemirror/theme-one-dark": "^6.0.0", - "@uiw/codemirror-extensions-basic-setup": "4.25.11", - "codemirror": "^6.0.0" - }, - "funding": { - "url": "https://jaywcjlove.github.io/#/sponsor" - }, - "peerDependencies": { - "@babel/runtime": ">=7.11.0", - "@codemirror/state": ">=6.0.0", - "@codemirror/theme-one-dark": ">=6.0.0", - "@codemirror/view": ">=6.0.0", - "codemirror": ">=6.0.0", - "react": ">=17.0.0", - "react-dom": ">=17.0.0" - } - }, - "node_modules/@ungap/structured-clone": { - "version": "1.3.3", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@ungap/structured-clone/-/structured-clone-1.3.3.tgz", - "integrity": "sha1-CUBB4aTLGYfwODNUISgayL45C8w=", - "license": "ISC" + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" }, - "node_modules/@vitejs/plugin-react": { - "version": "6.0.3", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@vitejs/plugin-react/-/plugin-react-6.0.3.tgz", - "integrity": "sha1-VfHX9VhTTRCu8DwAfcIIt8N3HOQ=", + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", "dev": true, "license": "MIT", - "dependencies": { - "@rolldown/pluginutils": "^1.0.1" - }, "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">=12.0.0" }, "peerDependencies": { - "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", - "babel-plugin-react-compiler": "^1.0.0", - "vite": "^8.0.0" + "picomatch": "^3 || ^4" }, "peerDependenciesMeta": { - "@rolldown/plugin-babel": { - "optional": true - }, - "babel-plugin-react-compiler": { + "picomatch": { "optional": true } } }, - "node_modules/@vitest/expect": { - "version": "4.1.10", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@vitest/expect/-/expect-4.1.10.tgz", - "integrity": "sha1-eZwG/ES7DPfieEE3tifFzBcyhdQ=", + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", "dev": true, "license": "MIT", "dependencies": { - "@standard-schema/spec": "^1.1.0", - "@types/chai": "^5.2.2", - "@vitest/spy": "4.1.10", - "@vitest/utils": "4.1.10", - "chai": "^6.2.2", - "tinyrainbow": "^3.1.0" + "flat-cache": "^4.0.0" }, - "funding": { - "url": "https://opencollective.com/vitest" + "engines": { + "node": ">=16.0.0" } }, - "node_modules/@vitest/mocker": { - "version": "4.1.10", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@vitest/mocker/-/mocker-4.1.10.tgz", - "integrity": "sha1-JBOYerTNf6HCthS0BMQHv2rR6tE=", + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "4.1.10", - "estree-walker": "^3.0.3", - "magic-string": "^0.30.21" - }, - "funding": { - "url": "https://opencollective.com/vitest" + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" }, - "peerDependencies": { - "msw": "^2.4.9", - "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + "engines": { + "node": ">=10" }, - "peerDependenciesMeta": { - "msw": { - "optional": true - }, - "vite": { - "optional": true - } + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@vitest/pretty-format": { - "version": "4.1.10", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@vitest/pretty-format/-/pretty-format-4.1.10.tgz", - "integrity": "sha1-dVQucnOgjMEP1Nja1OPrHxbNlYw=", + "node_modules/fix-dts-default-cjs-exports": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fix-dts-default-cjs-exports/-/fix-dts-default-cjs-exports-1.0.1.tgz", + "integrity": "sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==", "dev": true, "license": "MIT", "dependencies": { - "tinyrainbow": "^3.1.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" + "magic-string": "^0.30.17", + "mlly": "^1.7.4", + "rollup": "^4.34.8" } }, - "node_modules/@vitest/runner": { - "version": "4.1.10", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@vitest/runner/-/runner-4.1.10.tgz", - "integrity": "sha1-/r8KIakWhCFCLRlVNw5gb+q2A1U=", + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "4.1.10", - "pathe": "^2.0.3" + "flatted": "^3.2.9", + "keyv": "^4.5.4" }, - "funding": { - "url": "https://opencollective.com/vitest" + "engines": { + "node": ">=16" } }, - "node_modules/@vitest/snapshot": { - "version": "4.1.10", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@vitest/snapshot/-/snapshot-4.1.10.tgz", - "integrity": "sha1-fj6f7H1NRyMuSTz9y9IXDeQ3HAQ=", + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/pretty-format": "4.1.10", - "@vitest/utils": "4.1.10", - "magic-string": "^0.30.21", - "pathe": "^2.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } + "license": "ISC" }, - "node_modules/@vitest/spy": { - "version": "4.1.10", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@vitest/spy/-/spy-4.1.10.tgz", - "integrity": "sha1-XAv6l7Vrup43QDyXbbd2/2q1b2U=", + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "dev": true, + "hasInstallScript": true, "license": "MIT", - "funding": { - "url": "https://opencollective.com/vitest" + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, - "node_modules/@vitest/utils": { - "version": "4.1.10", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@vitest/utils/-/utils-4.1.10.tgz", - "integrity": "sha1-/8cQVfGL/Msf0FhjZevCgkiS5AM=", + "node_modules/glob": { + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", "dev": true, - "license": "MIT", + "license": "BlueOak-1.0.0", "dependencies": { - "@vitest/pretty-format": "4.1.10", - "convert-source-map": "^2.0.0", - "tinyrainbow": "^3.1.0" + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" }, "funding": { - "url": "https://opencollective.com/vitest" + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/accepts": { - "version": "2.0.0", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/accepts/-/accepts-2.0.0.tgz", - "integrity": "sha1-u89LpQdUZ/PyEx6rPP/HPC9deJU=", - "license": "MIT", + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", "dependencies": { - "mime-types": "^3.0.0", - "negotiator": "^1.0.0" + "is-glob": "^4.0.3" }, "engines": { - "node": ">= 0.6" + "node": ">=10.13.0" } }, - "node_modules/acorn": { - "version": "8.17.0", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/acorn/-/acorn-8.17.0.tgz", - "integrity": "sha1-F4WtuE+vjYrdEDabk4Jvwr0I8f4=", + "node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", "dev": true, "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, "engines": { - "node": ">=0.4.0" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha1-ftW7VZCLOy8bxVxq8WU7rafweTc=", + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, "license": "MIT", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + "engines": { + "node": ">=8" } }, - "node_modules/ajv": { - "version": "8.20.0", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/ajv/-/ajv-8.20.0.tgz", - "integrity": "sha1-MEs2Nq3Yi6fZNnYN1Q7OAG3qlfk=", + "node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" + "engines": { + "node": ">= 4" } }, - "node_modules/ajv-formats": { - "version": "3.0.1", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/ajv-formats/-/ajv-formats-3.0.1.tgz", - "integrity": "sha1-PV3HYryhdnnDwup+kK1rdTIwlXg=", + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, "license": "MIT", "dependencies": { - "ajv": "^8.0.0" - }, - "peerDependencies": { - "ajv": "^8.0.0" + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" }, - "peerDependenciesMeta": { - "ajv": { - "optional": true - } - } - }, - "node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha1-YCFu6kZNhkWXzigyAAc4oFiWUME=", - "license": "MIT", "engines": { - "node": ">=12" + "node": ">=6" }, "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha1-wETV3MUhoHZBNHJZehrLHxA8QEE=", + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, "license": "MIT", "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "node": ">=0.8.19" } }, - "node_modules/any-promise": { - "version": "1.3.0", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/any-promise/-/any-promise-1.3.0.tgz", - "integrity": "sha1-q8av7tzqUugJzcA3au0845Y10X8=", - "dev": true, - "license": "MIT" - }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha1-JG9Q88p4oyQPbJl+ipvR6sSeSzg=", - "dev": true, - "license": "Python-2.0" - }, - "node_modules/assertion-error": { - "version": "2.0.1", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/assertion-error/-/assertion-error-2.0.1.tgz", - "integrity": "sha1-9kGhlrM1aQsQcL8AtudZP+wZC/c=", + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", "dev": true, "license": "MIT", "engines": { - "node": ">=12" + "node": ">=0.10.0" } }, - "node_modules/bail": { - "version": "2.0.2", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/bail/-/bail-2.0.2.tgz", - "integrity": "sha1-0m9c2P5db4MqMVF7n3w1YEC6bV0=", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha1-v7EGYv7tgZaixi58aOF3IMJ0F5o=", + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "dev": true, "license": "MIT", - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/body-parser": { - "version": "2.3.0", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/body-parser/-/body-parser-2.3.0.tgz", - "integrity": "sha1-bYZi9NjDNgKLismqJCUbDKZLpDc=", - "license": "MIT", "dependencies": { - "bytes": "^3.1.2", - "content-type": "^2.0.0", - "debug": "^4.4.3", - "http-errors": "^2.0.1", - "iconv-lite": "^0.7.2", - "on-finished": "^2.4.1", - "qs": "^6.15.2", - "raw-body": "^3.0.2", - "type-is": "^2.1.0" + "is-extglob": "^2.1.1" }, "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" + "node": ">=0.10.0" } }, - "node_modules/body-parser/node_modules/content-type": { + "node_modules/isexe": { "version": "2.0.0", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/content-type/-/content-type-2.0.0.tgz", - "integrity": "sha1-L7Pt5p3/oK94ynxM51iWgGOLVt8=", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/joycon": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", + "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==", + "dev": true, "license": "MIT", "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" + "node": ">=10" } }, - "node_modules/brace-expansion": { - "version": "5.0.7", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/brace-expansion/-/brace-expansion-5.0.7.tgz", - "integrity": "sha1-Gw5GlltHna1lr3N7SgJ5CgVJgzc=", + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, "license": "MIT", "dependencies": { - "balanced-match": "^4.0.2" + "argparse": "^2.0.1" }, - "engines": { - "node": "18 || 20 || >=22" + "bin": { + "js-yaml": "bin/js-yaml.js" } }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/braces/-/braces-3.0.3.tgz", - "integrity": "sha1-SQMy9AkZRSJy1VqEgK3AxEE1h4k=", + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", "dev": true, "license": "MIT", "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" + "json-buffer": "3.0.1" } }, - "node_modules/bundle-require": { - "version": "5.1.0", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/bundle-require/-/bundle-require-5.1.0.tgz", - "integrity": "sha1-jbZvQZUNo9d68e8zIvTD4EAJ+u4=", + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", "dev": true, "license": "MIT", "dependencies": { - "load-tsconfig": "^0.2.3" + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" }, "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "peerDependencies": { - "esbuild": ">=0.18" + "node": ">= 0.8.0" } }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha1-iwvuuYYFrfGxKPpDhkA8AJ4CIaU=", + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, "license": "MIT", "engines": { - "node": ">= 0.8" + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" } }, - "node_modules/cac": { - "version": "6.7.14", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/cac/-/cac-6.7.14.tgz", - "integrity": "sha1-gE4eb1Bu42PLDjzLsJytXdmHCVk=", + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } + "license": "MIT" }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha1-S1QowiK+mF15w9gmV0edvgtZstY=", + "node_modules/load-tsconfig": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/load-tsconfig/-/load-tsconfig-0.2.5.tgz", + "integrity": "sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==", + "dev": true, "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, "engines": { - "node": ">= 0.4" + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" } }, - "node_modules/call-bound": { - "version": "1.0.4", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha1-I43pNdKippKSjFOMfM+pEGf9Bio=", + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, "license": "MIT", "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" + "p-locate": "^5.0.0" }, "engines": { - "node": ">= 0.4" + "node": ">=10" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha1-s2MKvYlDQy9Us/BRkjjjPNffL3M=", + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } + "license": "MIT" }, - "node_modules/ccount": { - "version": "2.0.1", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/ccount/-/ccount-2.0.1.tgz", - "integrity": "sha1-F6O/gjAuCHDW2kOgExGovAKj7PU=", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" }, - "node_modules/chai": { - "version": "6.2.2", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/chai/-/chai-6.2.2.tgz", - "integrity": "sha1-rkG1LJrKh3NFBTYnF/MlX6zaNg4=", + "node_modules/lru-cache": { + "version": "11.3.6", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.6.tgz", + "integrity": "sha512-Gf/KoL3C/MlI7Bt0PGI9I+TeTC/I6r/csU58N4BSNc4lppLBeKsOdFYkK+dX0ABDUMJNfCHTyPpzwwO21Awd3A==", "dev": true, - "license": "MIT", + "license": "BlueOak-1.0.0", "engines": { - "node": ">=18" + "node": "20 || >=22" } }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha1-qsTit3NKdAhnrrFr8CqtVWoeegE=", + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", "dev": true, "license": "MIT", "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "@jridgewell/sourcemap-codec": "^1.5.5" } }, - "node_modules/chalk/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha1-7dgDYornHATIWuegkG7a00tkiTc=", + "node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", "dev": true, - "license": "MIT", + "license": "BlueOak-1.0.0", "dependencies": { - "color-convert": "^2.0.1" + "brace-expansion": "^5.0.5" }, "engines": { - "node": ">=8" + "node": "18 || 20 || >=22" }, "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/character-entities": { - "version": "2.0.2", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/character-entities/-/character-entities-2.0.2.tgz", - "integrity": "sha1-LQnC5yzZUjB2zLIRV9/2atQ/zCI=", + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mlly": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.2.tgz", + "integrity": "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==", + "dev": true, "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" + "dependencies": { + "acorn": "^8.16.0", + "pathe": "^2.0.3", + "pkg-types": "^1.3.1", + "ufo": "^1.6.3" } }, - "node_modules/character-entities-html4": { - "version": "2.1.0", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/character-entities-html4/-/character-entities-html4-2.1.0.tgz", - "integrity": "sha1-HxrblAyXGksiujndymthjcblays=", + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" } }, - "node_modules/character-entities-legacy": { - "version": "3.0.0", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", - "integrity": "sha1-dryDqQc4kB17wiOp6TdZ/dVgEls=", + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, - "node_modules/character-reference-invalid": { - "version": "2.0.1", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", - "integrity": "sha1-hcZrBB5DtHIQ+vQBJ4q/gIrEXLk=", + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" + "engines": { + "node": ">=0.10.0" } }, - "node_modules/chart.js": { - "version": "4.5.1", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/chart.js/-/chart.js-4.5.1.tgz", - "integrity": "sha1-Gd0amjhqP2OXaRZyIxy1/JwFLDU=", + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, "license": "MIT", "dependencies": { - "@kurkle/color": "^0.3.0" + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" }, "engines": { - "pnpm": ">=8" + "node": ">= 0.8.0" } }, - "node_modules/chokidar": { - "version": "4.0.3", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/chokidar/-/chokidar-4.0.3.tgz", - "integrity": "sha1-e+N6TAPJruHs/oYqSiOyxwwgXTA=", + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", "dev": true, "license": "MIT", "dependencies": { - "readdirp": "^4.0.1" + "yocto-queue": "^0.1.0" }, "engines": { - "node": ">= 14.16.0" + "node": ">=10" }, "funding": { - "url": "https://paulmillr.com/funding/" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/cliui": { - "version": "9.0.1", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/cliui/-/cliui-9.0.1.tgz", - "integrity": "sha1-b3iQ84b28feZU63B943sRvzC0pE=", - "license": "ISC", + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", "dependencies": { - "string-width": "^7.2.0", - "strip-ansi": "^7.1.0", - "wrap-ansi": "^9.0.0" + "p-limit": "^3.0.2" }, "engines": { - "node": ">=20" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/codemirror": { - "version": "6.0.2", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/codemirror/-/codemirror-6.0.2.tgz", - "integrity": "sha1-TT/qGtYLZ1P5fKg18vSMaTaolG4=", - "license": "MIT", - "dependencies": { - "@codemirror/autocomplete": "^6.0.0", - "@codemirror/commands": "^6.0.0", - "@codemirror/language": "^6.0.0", - "@codemirror/lint": "^6.0.0", - "@codemirror/search": "^6.0.0", - "@codemirror/state": "^6.0.0", - "@codemirror/view": "^6.0.0" - } + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha1-ctOmjVmMm9s68q0ehPIdiWq9TeM=", + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", "dev": true, "license": "MIT", "dependencies": { - "color-name": "~1.1.4" + "callsites": "^3.0.0" }, "engines": { - "node": ">=7.0.0" + "node": ">=6" } }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha1-wqCah6y95pVD3m9j+jmVyCbFNqI=", + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", "dev": true, - "license": "MIT" - }, - "node_modules/comma-separated-tokens": { - "version": "2.0.3", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", - "integrity": "sha1-TonJRYrLYbyP7xn0UplzsjkoOe4=", "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" + "engines": { + "node": ">=8" } }, - "node_modules/commander": { - "version": "8.3.0", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/commander/-/commander-8.3.0.tgz", - "integrity": "sha1-SDfqGy2me5xhamevuw+v7lZ7ymY=", + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, "license": "MIT", "engines": { - "node": ">= 12" + "node": ">=8" } }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", "dev": true, - "license": "MIT" + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } }, - "node_modules/confbox": { - "version": "0.1.8", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/confbox/-/confbox-0.1.8.tgz", - "integrity": "sha1-gg1z07PILZvZEGUsXU1Znvj/iwY=", + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", "dev": true, "license": "MIT" }, - "node_modules/consola": { - "version": "3.4.2", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/consola/-/consola-3.4.2.tgz", - "integrity": "sha1-WvEQFFOXu2ev2rdwE/3DTK5ZDqc=", + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", "dev": true, "license": "MIT", "engines": { - "node": "^14.18.0 || >=16.10.0" + "node": ">= 14.16" } }, - "node_modules/content-disposition": { - "version": "1.1.0", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/content-disposition/-/content-disposition-1.1.0.tgz", - "integrity": "sha1-89t4nHUtRVZMx+nh4LMXkNSjjhc=", + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, "license": "MIT", "engines": { - "node": ">=18" + "node": ">=12" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" + "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/content-type": { - "version": "1.0.5", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha1-i3cxYmVtHRCGeEyPI6VM5tc9eRg=", + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, "license": "MIT", "engines": { - "node": ">= 0.6" + "node": ">= 6" } }, - "node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha1-S1YPZJ/E6RjdCrdc9JYei8iC2Co=", + "node_modules/pkg-types": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", + "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", "dev": true, - "license": "MIT" - }, - "node_modules/cookie": { - "version": "0.7.2", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/cookie/-/cookie-0.7.2.tgz", - "integrity": "sha1-VWNpxHKiupEPKXmJG1JrNDYjftc=", "license": "MIT", - "engines": { - "node": ">= 0.6" + "dependencies": { + "confbox": "^0.1.8", + "mlly": "^1.7.4", + "pathe": "^2.0.1" } }, - "node_modules/cookie-signature": { - "version": "1.2.2", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/cookie-signature/-/cookie-signature-1.2.2.tgz", - "integrity": "sha1-V8f8PMKTrKuf7FTXPhVpDr5KF5M=", + "node_modules/postcss": { + "version": "8.5.14", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz", + "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, "engines": { - "node": ">=6.6.0" + "node": "^10 || ^12 || >=14" } }, - "node_modules/cors": { - "version": "2.8.6", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/cors/-/cors-2.8.6.tgz", - "integrity": "sha1-/13Wm9leVHUDgg0pq6T4+vjf7JY=", + "node_modules/postcss-load-config": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], "license": "MIT", "dependencies": { - "object-assign": "^4", - "vary": "^1" + "lilconfig": "^3.1.1" }, "engines": { - "node": ">= 0.10" + "node": ">= 18" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } } }, - "node_modules/crelt": { - "version": "1.0.7", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/crelt/-/crelt-1.0.7.tgz", - "integrity": "sha1-O0QbLd+nMWHWoncKpM1nf4leryg=", - "license": "MIT" - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha1-ilj+ePANzXDDcEUXWd+/rwPo7p8=", + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, "engines": { - "node": ">= 8" + "node": ">= 0.8.0" } }, - "node_modules/csstype": { - "version": "3.2.3", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/csstype/-/csstype-3.2.3.tgz", - "integrity": "sha1-7EjA8+mT5QZIyG2lWeJhCZXPmJo=", - "license": "MIT" - }, - "node_modules/d3-array": { - "version": "3.2.4", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/d3-array/-/d3-array-3.2.4.tgz", - "integrity": "sha1-Ff7DOyN/l6xdfJhtx32ic6jtC7U=", - "license": "ISC", - "dependencies": { - "internmap": "1 - 2" + "node_modules/prettier": { + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.3.tgz", + "integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" }, "engines": { - "node": ">=12" + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" } }, - "node_modules/d3-color": { - "version": "3.1.0", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/d3-color/-/d3-color-3.1.0.tgz", - "integrity": "sha1-OVsoM9+scVB/EqwvevI7+BneJOI=", - "license": "ISC", + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", "engines": { - "node": ">=12" + "node": ">=6" } }, - "node_modules/d3-delaunay": { - "version": "6.0.4", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/d3-delaunay/-/d3-delaunay-6.0.4.tgz", - "integrity": "sha1-mBaQOHM6ClurvtpVBU95W7nkpYs=", - "license": "ISC", - "dependencies": { - "delaunator": "5" - }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", "engines": { - "node": ">=12" + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" } }, - "node_modules/d3-dispatch": { - "version": "3.0.1", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/d3-dispatch/-/d3-dispatch-3.0.1.tgz", - "integrity": "sha1-X8dShOnCN1w2yDlBGgz1UMv8TV4=", - "license": "ISC", + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", "engines": { - "node": ">=12" + "node": ">=4" } }, - "node_modules/d3-dsv": { - "version": "3.0.1", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/d3-dsv/-/d3-dsv-3.0.1.tgz", - "integrity": "sha1-xjr5ePTWoNCEpSpnOSK+IWB4m3M=", - "license": "ISC", + "node_modules/rimraf": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.1.3.tgz", + "integrity": "sha512-LKg+Cr2ZF61fkcaK1UdkH2yEBBKnYjTyWzTJT6KNPcSPaiT7HSdhtMXQuN5wkTX0Xu72KQ1l8S42rlmexS2hSA==", + "dev": true, + "license": "BlueOak-1.0.0", "dependencies": { - "commander": "7", - "iconv-lite": "0.6", - "rw": "1" + "glob": "^13.0.3", + "package-json-from-dist": "^1.0.1" }, "bin": { - "csv2json": "bin/dsv2json.js", - "csv2tsv": "bin/dsv2dsv.js", - "dsv2dsv": "bin/dsv2dsv.js", - "dsv2json": "bin/dsv2json.js", - "json2csv": "bin/json2dsv.js", - "json2dsv": "bin/json2dsv.js", - "json2tsv": "bin/json2dsv.js", - "tsv2csv": "bin/dsv2dsv.js", - "tsv2json": "bin/dsv2json.js" + "rimraf": "dist/esm/bin.mjs" }, "engines": { - "node": ">=12" - } - }, - "node_modules/d3-dsv/node_modules/commander": { - "version": "7.2.0", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/commander/-/commander-7.2.0.tgz", - "integrity": "sha1-o2y1fQtQHOEI5NIFWaFQo5HZerc=", - "license": "MIT", - "engines": { - "node": ">= 10" + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/d3-dsv/node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha1-pS+AvzjaGVLrXGgXkHGYcaGnJQE=", + "node_modules/rollup": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.3.tgz", + "integrity": "sha512-pAQK9HalE84QSm4Po3EmWIZPd3FnjkShVkiMlz1iligWYkWQ7wHYd1PF/T7QZ5TVSD6uSTon5gBVMSM4JfBV+A==", + "dev": true, "license": "MIT", "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" + "@types/estree": "1.0.8" }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/d3-force": { - "version": "3.0.0", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/d3-force/-/d3-force-3.0.0.tgz", - "integrity": "sha1-Piuhph5wiI/j2RlOMNbRTuzhVcQ=", - "license": "ISC", - "dependencies": { - "d3-dispatch": "1 - 3", - "d3-quadtree": "1 - 3", - "d3-timer": "1 - 3" + "bin": { + "rollup": "dist/bin/rollup" }, "engines": { - "node": ">=12" + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.3", + "@rollup/rollup-android-arm64": "4.60.3", + "@rollup/rollup-darwin-arm64": "4.60.3", + "@rollup/rollup-darwin-x64": "4.60.3", + "@rollup/rollup-freebsd-arm64": "4.60.3", + "@rollup/rollup-freebsd-x64": "4.60.3", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.3", + "@rollup/rollup-linux-arm-musleabihf": "4.60.3", + "@rollup/rollup-linux-arm64-gnu": "4.60.3", + "@rollup/rollup-linux-arm64-musl": "4.60.3", + "@rollup/rollup-linux-loong64-gnu": "4.60.3", + "@rollup/rollup-linux-loong64-musl": "4.60.3", + "@rollup/rollup-linux-ppc64-gnu": "4.60.3", + "@rollup/rollup-linux-ppc64-musl": "4.60.3", + "@rollup/rollup-linux-riscv64-gnu": "4.60.3", + "@rollup/rollup-linux-riscv64-musl": "4.60.3", + "@rollup/rollup-linux-s390x-gnu": "4.60.3", + "@rollup/rollup-linux-x64-gnu": "4.60.3", + "@rollup/rollup-linux-x64-musl": "4.60.3", + "@rollup/rollup-openbsd-x64": "4.60.3", + "@rollup/rollup-openharmony-arm64": "4.60.3", + "@rollup/rollup-win32-arm64-msvc": "4.60.3", + "@rollup/rollup-win32-ia32-msvc": "4.60.3", + "@rollup/rollup-win32-x64-gnu": "4.60.3", + "@rollup/rollup-win32-x64-msvc": "4.60.3", + "fsevents": "~2.3.2" } }, - "node_modules/d3-format": { - "version": "3.1.2", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/d3-format/-/d3-format-3.1.2.tgz", - "integrity": "sha1-Af20a1i+sfVbELQq1wtuNE1esq4=", - "license": "ISC", - "engines": { - "node": ">=12" - } + "node_modules/rollup/node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" }, - "node_modules/d3-geo": { - "version": "3.1.1", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/d3-geo/-/d3-geo-3.1.1.tgz", - "integrity": "sha1-YCfPUSRvmy69ZPmeAdx8M2QDOk0=", + "node_modules/semver": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", + "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", + "dev": true, "license": "ISC", - "dependencies": { - "d3-array": "2.5.0 - 3" + "bin": { + "semver": "bin/semver.js" }, "engines": { - "node": ">=12" + "node": ">=10" } }, - "node_modules/d3-geo-projection": { - "version": "4.0.0", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/d3-geo-projection/-/d3-geo-projection-4.0.0.tgz", - "integrity": "sha1-3CKeXq140xhppOh88fRb0nFsSMo=", - "license": "ISC", + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", "dependencies": { - "commander": "7", - "d3-array": "1 - 3", - "d3-geo": "1.12.0 - 3" - }, - "bin": { - "geo2svg": "bin/geo2svg.js", - "geograticule": "bin/geograticule.js", - "geoproject": "bin/geoproject.js", - "geoquantize": "bin/geoquantize.js", - "geostitch": "bin/geostitch.js" + "shebang-regex": "^3.0.0" }, "engines": { - "node": ">=12" + "node": ">=8" } }, - "node_modules/d3-geo-projection/node_modules/commander": { - "version": "7.2.0", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/commander/-/commander-7.2.0.tgz", - "integrity": "sha1-o2y1fQtQHOEI5NIFWaFQo5HZerc=", + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, "license": "MIT", "engines": { - "node": ">= 10" + "node": ">=8" } }, - "node_modules/d3-hierarchy": { - "version": "3.1.2", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", - "integrity": "sha1-sBzULB7tPUbbd6WWbPcm+MCRYMY=", - "license": "ISC", - "engines": { - "node": ">=12" - } + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" }, - "node_modules/d3-interpolate": { - "version": "3.0.1", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/d3-interpolate/-/d3-interpolate-3.0.1.tgz", - "integrity": "sha1-PEeqWzLFs9+1bvP9Q0IHimMrQA0=", - "license": "ISC", - "dependencies": { - "d3-color": "1 - 3" - }, + "node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "dev": true, + "license": "BSD-3-Clause", "engines": { - "node": ">=12" + "node": ">= 12" } }, - "node_modules/d3-path": { - "version": "3.1.0", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/d3-path/-/d3-path-3.1.0.tgz", - "integrity": "sha1-It+TkDL7WnGuixgA1h3beFHEJSY=", - "license": "ISC", + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", "engines": { - "node": ">=12" + "node": ">=0.10.0" } }, - "node_modules/d3-quadtree": { - "version": "3.0.1", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/d3-quadtree/-/d3-quadtree-3.0.1.tgz", - "integrity": "sha1-bco+i+Kzk8mp1RTau9gKkt7vGk8=", - "license": "ISC", - "engines": { - "node": ">=12" - } + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" }, - "node_modules/d3-scale": { - "version": "4.0.2", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/d3-scale/-/d3-scale-4.0.2.tgz", - "integrity": "sha1-grOOjo/3CAdk+Nzsd71L45Nok5Y=", - "license": "ISC", - "dependencies": { - "d3-array": "2.10.0 - 3", - "d3-format": "1 - 3", - "d3-interpolate": "1.2.0 - 3", - "d3-time": "2.1.1 - 3", - "d3-time-format": "2 - 4" - }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", "engines": { - "node": ">=12" + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/d3-scale-chromatic": { - "version": "3.1.0", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", - "integrity": "sha1-NMOdopiyPCDgLxpLI5vQ8i5/ExQ=", - "license": "ISC", + "node_modules/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "dev": true, + "license": "MIT", "dependencies": { - "d3-color": "1 - 3", - "d3-interpolate": "1 - 3" + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" }, "engines": { - "node": ">=12" + "node": ">=16 || 14 >=14.17" } }, - "node_modules/d3-shape": { - "version": "3.2.0", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/d3-shape/-/d3-shape-3.2.0.tgz", - "integrity": "sha1-oag5y9m6RfKGdMadf4Vbz5HfxqU=", - "license": "ISC", + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", "dependencies": { - "d3-path": "^3.1.0" + "has-flag": "^4.0.0" }, "engines": { - "node": ">=12" + "node": ">=8" } }, - "node_modules/d3-time": { - "version": "3.1.0", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/d3-time/-/d3-time-3.1.0.tgz", - "integrity": "sha1-kxDbVumS48AXXh7zheVF5Iqbtcc=", - "license": "ISC", + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", "dependencies": { - "d3-array": "2 - 3" - }, - "engines": { - "node": ">=12" + "any-promise": "^1.0.0" } }, - "node_modules/d3-time-format": { - "version": "4.1.0", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/d3-time-format/-/d3-time-format-4.1.0.tgz", - "integrity": "sha1-erUlelBB0R7LT+cKXH0WoZW7QIo=", - "license": "ISC", + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", "dependencies": { - "d3-time": "1 - 3" + "thenify": ">= 3.1.0 < 4" }, "engines": { - "node": ">=12" + "node": ">=0.8" } }, - "node_modules/d3-timer": { - "version": "3.0.1", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/d3-timer/-/d3-timer-3.0.1.tgz", - "integrity": "sha1-YoTSonCChbGrt+IB7aQ4CvNeY7A=", - "license": "ISC", - "engines": { - "node": ">=12" - } + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/debug/-/debug-4.4.3.tgz", - "integrity": "sha1-xq5DLZvZZiWC/OCHCbA4xY6ePWo=", + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "dev": true, "license": "MIT", "dependencies": { - "ms": "^2.1.3" + "fdir": "^6.5.0", + "picomatch": "^4.0.4" }, "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/decode-named-character-reference": { - "version": "1.3.0", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz", - "integrity": "sha1-PkBgN2CHTC5YZ2kbWZ1zp9oltT8=", - "license": "MIT", - "dependencies": { - "character-entities": "^2.0.0" + "node": ">=12.0.0" }, "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" + "url": "https://github.com/sponsors/SuperchupuDev" } }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha1-pvLc5hL63S7x9Rm3NVHxfoUZmDE=", + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", "dev": true, - "license": "MIT" - }, - "node_modules/delaunator": { - "version": "5.1.0", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/delaunator/-/delaunator-5.1.0.tgz", - "integrity": "sha1-0TJx+/Ov9nU/nqbiNVV/IJAQRuo=", - "license": "ISC", - "dependencies": { - "robust-predicates": "^3.0.2" - } - }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/depd/-/depd-2.0.0.tgz", - "integrity": "sha1-tpYWPMdXVg0JzyLMj60Vcbeedt8=", "license": "MIT", "engines": { - "node": ">= 0.8" + "node": "^18.0.0 || >=20.0.0" } }, - "node_modules/dequal": { - "version": "2.0.3", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/dequal/-/dequal-2.0.3.tgz", - "integrity": "sha1-JkQhTxmX057Q7g7OcjNUkKesZ74=", + "node_modules/tinyrainbow": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", + "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", + "dev": true, "license": "MIT", "engines": { - "node": ">=6" + "node": ">=14.0.0" } }, - "node_modules/detect-libc": { - "version": "2.1.2", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha1-aJxdzcGQDvVYOky59te0c3QgdK0=", + "node_modules/tinyspy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", + "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", "engines": { - "node": ">=8" + "node": ">=14.0.0" } }, - "node_modules/devlop": { - "version": "1.1.0", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/devlop/-/devlop-1.1.0.tgz", - "integrity": "sha1-TbfCyk3G4Og0wwvnDJS7yXbccBg=", + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, "license": "MIT", - "dependencies": { - "dequal": "^2.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" + "bin": { + "tree-kill": "cli.js" } }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha1-165mfh3INIL4tw/Q9u78UNow9Yo=", + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, "engines": { - "node": ">= 0.4" - } - }, - "node_modules/echarts": { - "version": "6.1.0", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/echarts/-/echarts-6.1.0.tgz", - "integrity": "sha1-rg9oWQ9eu9co2QCQfCes3nxUVtE=", - "license": "Apache-2.0", - "dependencies": { - "tslib": "2.3.0", - "zrender": "6.1.0" + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" } }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=", - "license": "MIT" - }, - "node_modules/emoji-regex": { - "version": "10.6.0", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/emoji-regex/-/emoji-regex-10.6.0.tgz", - "integrity": "sha1-vz1uj3+P0ipl2XA0dbwBRzV6aw0=", - "license": "MIT" + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, + "license": "Apache-2.0" }, - "node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha1-e46omAd9fkCdOsRUdOo46vCFelg=", + "node_modules/tsup": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/tsup/-/tsup-8.5.1.tgz", + "integrity": "sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing==", + "dev": true, "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/entities": { - "version": "6.0.1", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/entities/-/entities-6.0.1.tgz", - "integrity": "sha1-wow0pDN5yn9h0HQTCy9fcCCjBpQ=", - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.12" + "dependencies": { + "bundle-require": "^5.1.0", + "cac": "^6.7.14", + "chokidar": "^4.0.3", + "consola": "^3.4.0", + "debug": "^4.4.0", + "esbuild": "^0.27.0", + "fix-dts-default-cjs-exports": "^1.0.0", + "joycon": "^3.1.1", + "picocolors": "^1.1.1", + "postcss-load-config": "^6.0.1", + "resolve-from": "^5.0.0", + "rollup": "^4.34.8", + "source-map": "^0.7.6", + "sucrase": "^3.35.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.11", + "tree-kill": "^1.2.2" + }, + "bin": { + "tsup": "dist/cli-default.js", + "tsup-node": "dist/cli-node.js" }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha1-mD6y+aZyTpMD9hrd8BHHLgngsPo=", - "license": "MIT", "engines": { - "node": ">= 0.4" + "node": ">=18" + }, + "peerDependencies": { + "@microsoft/api-extractor": "^7.36.0", + "@swc/core": "^1", + "postcss": "^8.4.12", + "typescript": ">=4.5.0" + }, + "peerDependenciesMeta": { + "@microsoft/api-extractor": { + "optional": true + }, + "@swc/core": { + "optional": true + }, + "postcss": { + "optional": true + }, + "typescript": { + "optional": true + } } }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha1-BfdaJdq5jk+x3NXhRywFRtUFfI8=", + "node_modules/tsup/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, "license": "MIT", "engines": { - "node": ">= 0.4" + "node": ">=8" } }, - "node_modules/es-module-lexer": { - "version": "2.3.1", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/es-module-lexer/-/es-module-lexer-2.3.1.tgz", - "integrity": "sha1-W/LfBpmdu+XwBqX0ahH7n1t7ORs=", + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", "dev": true, - "license": "MIT" - }, - "node_modules/es-object-atoms": { - "version": "1.1.2", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/es-object-atoms/-/es-object-atoms-1.1.2.tgz", - "integrity": "sha1-otCzcyBXJN+lJdI7DD4bHKWCyZs=", "license": "MIT", "dependencies": { - "es-errors": "^1.3.0" + "prelude-ls": "^1.2.1" }, "engines": { - "node": ">= 0.4" + "node": ">= 0.8.0" } }, - "node_modules/esbuild": { - "version": "0.28.1", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/esbuild/-/esbuild-0.28.1.tgz", - "integrity": "sha1-70W0Y0ycnZeilq6kEUpfmED5VXg=", + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, - "hasInstallScript": true, - "license": "MIT", + "license": "Apache-2.0", "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.28.1", - "@esbuild/android-arm": "0.28.1", - "@esbuild/android-arm64": "0.28.1", - "@esbuild/android-x64": "0.28.1", - "@esbuild/darwin-arm64": "0.28.1", - "@esbuild/darwin-x64": "0.28.1", - "@esbuild/freebsd-arm64": "0.28.1", - "@esbuild/freebsd-x64": "0.28.1", - "@esbuild/linux-arm": "0.28.1", - "@esbuild/linux-arm64": "0.28.1", - "@esbuild/linux-ia32": "0.28.1", - "@esbuild/linux-loong64": "0.28.1", - "@esbuild/linux-mips64el": "0.28.1", - "@esbuild/linux-ppc64": "0.28.1", - "@esbuild/linux-riscv64": "0.28.1", - "@esbuild/linux-s390x": "0.28.1", - "@esbuild/linux-x64": "0.28.1", - "@esbuild/netbsd-arm64": "0.28.1", - "@esbuild/netbsd-x64": "0.28.1", - "@esbuild/openbsd-arm64": "0.28.1", - "@esbuild/openbsd-x64": "0.28.1", - "@esbuild/openharmony-arm64": "0.28.1", - "@esbuild/sunos-x64": "0.28.1", - "@esbuild/win32-arm64": "0.28.1", - "@esbuild/win32-ia32": "0.28.1", - "@esbuild/win32-x64": "0.28.1" - } - }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha1-ARo/aYVroYnf+n3I/M6Z0qh5A+U=", - "license": "MIT", "engines": { - "node": ">=6" + "node": ">=14.17" } }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=", - "license": "MIT" - }, - "node_modules/escape-string-regexp": { - "version": "5.0.0", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", - "integrity": "sha1-RoMSa1ALYXYvLb66zhgG6L4xscg=", + "node_modules/typescript-eslint": { + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.59.3.tgz", + "integrity": "sha512-KgusgyDgG4LI8Ih/sWaCtZ06tckLAS5CvT5A4D1Q7bYVoAAyzwiZvE4BmwDHkhRVkvhRBepKeASoFzQetha7Fg==", + "dev": true, "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.59.3", + "@typescript-eslint/parser": "8.59.3", + "@typescript-eslint/typescript-estree": "8.59.3", + "@typescript-eslint/utils": "8.59.3" + }, "engines": { - "node": ">=12" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/eslint": { - "version": "9.39.5", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/eslint/-/eslint-9.39.5.tgz", - "integrity": "sha1-Kk48iw91MZbvrpQ8j/qocw/Go/o=", + "node_modules/ufo": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.4.tgz", + "integrity": "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==", + "dev": true, + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.8.0", - "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.21.2", - "@eslint/config-helpers": "^0.4.2", - "@eslint/core": "^0.17.0", - "@eslint/eslintrc": "^3.3.6", - "@eslint/js": "9.39.5", - "@eslint/plugin-kit": "^0.4.1", - "@humanfs/node": "^0.16.6", - "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.4.2", - "@types/estree": "^1.0.6", - "ajv": "^6.14.0", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.6", - "debug": "^4.3.2", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.4.0", - "eslint-visitor-keys": "^4.2.1", - "espree": "^10.4.0", - "esquery": "^1.5.0", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^8.0.0", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.5", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3" + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" }, "bin": { - "eslint": "bin/eslint.js" + "vite": "bin/vite.js" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^18.0.0 || >=20.0.0" }, "funding": { - "url": "https://eslint.org/donate" + "url": "https://github.com/vitejs/vite?sponsor=1" }, - "peerDependencies": { - "jiti": "*" + "optionalDependencies": { + "fsevents": "~2.3.3" }, - "peerDependenciesMeta": { - "jiti": { - "optional": true - } - } - }, - "node_modules/eslint-plugin-unused-imports": { - "version": "4.4.1", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/eslint-plugin-unused-imports/-/eslint-plugin-unused-imports-4.4.1.tgz", - "integrity": "sha1-qDHwopN9djHrowy4cJGrfTpdoOE=", - "dev": true, - "license": "MIT", "peerDependencies": { - "@typescript-eslint/eslint-plugin": "^8.0.0-0 || ^7.0.0 || ^6.0.0 || ^5.0.0", - "eslint": "^10.0.0 || ^9.0.0 || ^8.0.0" + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" }, "peerDependenciesMeta": { - "@typescript-eslint/eslint-plugin": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { "optional": true } } }, - "node_modules/eslint-scope": { - "version": "8.4.0", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/eslint-scope/-/eslint-scope-8.4.0.tgz", - "integrity": "sha1-iOZGogf61hQ2/6OetQUUcgBlXII=", + "node_modules/vite-node": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.9.tgz", + "integrity": "sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==", "dev": true, - "license": "BSD-2-Clause", + "license": "MIT", "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" + "cac": "^6.7.14", + "debug": "^4.3.7", + "es-module-lexer": "^1.5.4", + "pathe": "^1.1.2", + "vite": "^5.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^18.0.0 || >=20.0.0" }, "funding": { - "url": "https://opencollective.com/eslint" + "url": "https://opencollective.com/vitest" } }, - "node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha1-DNcv6FUOPC6uFWqWpN3c0cisWAA=", + "node_modules/vite-node/node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } + "license": "MIT" }, - "node_modules/eslint/node_modules/ajv": { - "version": "6.15.0", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/ajv/-/ajv-6.15.0.tgz", - "integrity": "sha1-B+mCx0YmFnqnoklcU4F4ktcTlJI=", + "node_modules/vite/node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], "dev": true, "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" } }, - "node_modules/eslint/node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha1-6D46fj8wCzTLnYf2FfoMvzV2kO4=", - "dev": true, - "license": "MIT" - }, - "node_modules/eslint/node_modules/brace-expansion": { - "version": "1.1.16", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/brace-expansion/-/brace-expansion-1.1.16.tgz", - "integrity": "sha1-cj06MMBVjCJavJ/Eeac+FOJsPC8=", + "node_modules/vite/node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], "dev": true, "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" } }, - "node_modules/eslint/node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha1-FLqDpdNz49MR5a/KKc9b+tllvzQ=", + "node_modules/vite/node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "android" + ], "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=12" } }, - "node_modules/eslint/node_modules/eslint-visitor-keys": { - "version": "4.2.1", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha1-TP6mD+fdCtjoFuHtAmwdUlG1EsE=", + "node_modules/vite/node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], "dev": true, - "license": "Apache-2.0", + "license": "MIT", + "optional": true, + "os": [ + "android" + ], "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" + "node": ">=12" } }, - "node_modules/eslint/node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha1-PNQOcp82Q/2HywTlC/DrcivFlvU=", + "node_modules/vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">= 4" + "node": ">=12" } }, - "node_modules/eslint/node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha1-afaofZUTq4u4/mO9sJecRI5oRmA=", + "node_modules/vite/node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } }, - "node_modules/eslint/node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha1-WAyI+NVEXyvWqo88re+g3nn71p4=", + "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": "*" + "node": ">=12" } }, - "node_modules/espree": { - "version": "10.4.0", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/espree/-/espree-10.4.0.tgz", - "integrity": "sha1-1U9JSdRikAWh+haNk3w/8ffiqDc=", + "node_modules/vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^8.15.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.1" - }, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" + "node": ">=12" } }, - "node_modules/espree/node_modules/eslint-visitor-keys": { - "version": "4.2.1", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha1-TP6mD+fdCtjoFuHtAmwdUlG1EsE=", + "node_modules/vite/node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], "dev": true, - "license": "Apache-2.0", + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" + "node": ">=12" } }, - "node_modules/esquery": { - "version": "1.7.0", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/esquery/-/esquery-1.7.0.tgz", - "integrity": "sha1-CNBI8mHw3e21uulfRoCUY9nJSW0=", + "node_modules/vite/node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "estraverse": "^5.1.0" - }, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=0.10" + "node": ">=12" } }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha1-eteWTWeauyi+5yzsY3WLHF0smSE=", + "node_modules/vite/node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "estraverse": "^5.2.0" - }, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=4.0" + "node": ">=12" } }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha1-LupSkHAvJquP5TcDcP+GyWXSESM=", + "node_modules/vite/node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estree-util-is-identifier-name": { - "version": "3.0.0", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz", - "integrity": "sha1-C170xP8TUIs03NAez6lF9h/OXb0=", "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" } }, - "node_modules/estree-walker": { - "version": "3.0.3", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/estree-walker/-/estree-walker-3.0.3.tgz", - "integrity": "sha1-Z8PlSexAKkh7T8GT0ZU6UkdSNA0=", + "node_modules/vite/node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], "dev": true, "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" } }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha1-dNLrTeC42hKTcRkQ1Qd1ubcQ72Q=", + "node_modules/vite/node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], "dev": true, - "license": "BSD-2-Clause", + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=0.10.0" + "node": ">=12" } }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/etag/-/etag-1.8.1.tgz", - "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=", + "node_modules/vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">= 0.6" + "node": ">=12" } }, - "node_modules/eventsource": { - "version": "3.0.7", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/eventsource/-/eventsource-3.0.7.tgz", - "integrity": "sha1-EVdiLi9Td7tq7yEUNycougwVaYk=", + "node_modules/vite/node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, "license": "MIT", - "dependencies": { - "eventsource-parser": "^3.0.1" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=18.0.0" + "node": ">=12" } }, - "node_modules/eventsource-parser": { - "version": "3.1.0", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/eventsource-parser/-/eventsource-parser-3.1.0.tgz", - "integrity": "sha1-ThmOuRzTM9Co3cwDZQKzYYol9Ek=", + "node_modules/vite/node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=18.0.0" + "node": ">=12" } }, - "node_modules/expect-type": { - "version": "1.4.0", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/expect-type/-/expect-type-1.4.0.tgz", - "integrity": "sha1-JO338MxppE0AhWe6RZSrlvPDo9Y=", + "node_modules/vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], "dev": true, - "license": "Apache-2.0", + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], "engines": { - "node": ">=12.0.0" + "node": ">=12" } }, - "node_modules/express": { - "version": "5.2.1", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/express/-/express-5.2.1.tgz", - "integrity": "sha1-jyHRW20yf5K0eU7PjLCKcvlWrAQ=", + "node_modules/vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "accepts": "^2.0.0", - "body-parser": "^2.2.1", - "content-disposition": "^1.0.0", - "content-type": "^1.0.5", - "cookie": "^0.7.1", - "cookie-signature": "^1.2.1", - "debug": "^4.4.0", - "depd": "^2.0.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "finalhandler": "^2.1.0", - "fresh": "^2.0.0", - "http-errors": "^2.0.0", - "merge-descriptors": "^2.0.0", - "mime-types": "^3.0.0", - "on-finished": "^2.4.1", - "once": "^1.4.0", - "parseurl": "^1.3.3", - "proxy-addr": "^2.0.7", - "qs": "^6.14.0", - "range-parser": "^1.2.1", - "router": "^2.2.0", - "send": "^1.1.0", - "serve-static": "^2.2.0", - "statuses": "^2.0.1", - "type-is": "^2.0.1", - "vary": "^1.1.2" - }, + "optional": true, + "os": [ + "openbsd" + ], "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" + "node": ">=12" } }, - "node_modules/express-rate-limit": { - "version": "8.6.0", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/express-rate-limit/-/express-rate-limit-8.6.0.tgz", - "integrity": "sha1-LmSrEDYdo1iBUZ6pK0JqQ/iqjHk=", + "node_modules/vite/node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "debug": "^4.4.3", - "ip-address": "^10.2.0" - }, + "optional": true, + "os": [ + "sunos" + ], "engines": { - "node": ">= 16" - }, - "funding": { - "url": "https://github.com/sponsors/express-rate-limit" - }, - "peerDependencies": { - "express": ">= 4.11" + "node": ">=12" } }, - "node_modules/extend": { - "version": "3.0.2", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/extend/-/extend-3.0.2.tgz", - "integrity": "sha1-+LETa0Bx+9jrFAr/hYsQGewpFfo=", - "license": "MIT" - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha1-On1WtVnWy8PrUSMlJE5hmmXGxSU=", - "license": "MIT" - }, - "node_modules/fast-json-patch": { - "version": "3.1.1", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/fast-json-patch/-/fast-json-patch-3.1.1.tgz", - "integrity": "sha1-hQZOobHr+Xo/etAeI/kzfnLGaUc=", - "license": "MIT" - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha1-h0v2nG9ATCtdmcSBNBOZ/VWJJjM=", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-uri": { - "version": "3.1.3", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/fast-uri/-/fast-uri-3.1.3.tgz", - "integrity": "sha1-9pWkDwBqulBWMVc6ACHdshGUrRE=", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } + "node_modules/vite/node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" ], - "license": "BSD-3-Clause" - }, - "node_modules/fault": { - "version": "1.0.4", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/fault/-/fault-1.0.4.tgz", - "integrity": "sha1-6vz8Cm0hT8lGAeFw3ymVSk+ELxM=", + "dev": true, "license": "MIT", - "dependencies": { - "format": "^0.2.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" } }, - "node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha1-7Sq5Z6MxreYvGNB32uGSaE1Q01A=", + "node_modules/vite/node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } + "node": ">=12" } }, - "node_modules/file-entry-cache": { - "version": "8.0.0", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/file-entry-cache/-/file-entry-cache-8.0.0.tgz", - "integrity": "sha1-d4e93PETG/+5JjbGlFe7wO3W2B8=", - "dev": true, - "license": "MIT", - "dependencies": { - "flat-cache": "^4.0.0" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha1-RCZdPKwH4+p9wkdRY4BkN1SgUpI=", - "dev": true, - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/finalhandler": { - "version": "2.1.1", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/finalhandler/-/finalhandler-2.1.1.tgz", - "integrity": "sha1-osUXplWYUrzbBtH4vX9Rto+tgJk=", - "license": "MIT", - "dependencies": { - "debug": "^4.4.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "on-finished": "^2.4.1", - "parseurl": "^1.3.3", - "statuses": "^2.0.1" - }, - "engines": { - "node": ">= 18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha1-TJKBnstwg1YeT0okCoa+UZj1Nvw=", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/fix-dts-default-cjs-exports": { - "version": "1.0.1", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/fix-dts-default-cjs-exports/-/fix-dts-default-cjs-exports-1.0.1.tgz", - "integrity": "sha1-lVy2s9UZaRxXgosHitrfLLkulUk=", - "dev": true, - "license": "MIT", - "dependencies": { - "magic-string": "^0.30.17", - "mlly": "^1.7.4", - "rollup": "^4.34.8" - } - }, - "node_modules/flat-cache": { - "version": "4.0.1", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/flat-cache/-/flat-cache-4.0.1.tgz", - "integrity": "sha1-Ds45/LFO4BL0sEEL0z3ZwfAREnw=", - "dev": true, - "license": "MIT", - "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.4" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/flatted": { - "version": "3.4.2", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/flatted/-/flatted-3.4.2.tgz", - "integrity": "sha1-9cI8EH8PN96NvfJPE3IrO5jVJyY=", - "dev": true, - "license": "ISC" - }, - "node_modules/flint-chart": { - "resolved": "packages/flint-js", - "link": true - }, - "node_modules/flint-chart-mcp": { - "resolved": "packages/flint-mcp", - "link": true - }, - "node_modules/flint-chart-site": { - "resolved": "site", - "link": true - }, - "node_modules/format": { - "version": "0.2.2", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/format/-/format-0.2.2.tgz", - "integrity": "sha1-1hcBB+nv3E7TDJ3DkBbflCtctYs=", - "engines": { - "node": ">=0.4.x" - } - }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha1-ImmTZCiq1MFcfr6XeahL8LKoGBE=", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fresh": { - "version": "2.0.0", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/fresh/-/fresh-2.0.0.tgz", - "integrity": "sha1-jdffahs6Gzpc8YbAWl3SZ2ImNaQ=", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha1-ysZAd4XQNnWipeGlMFxpezR9kNY=", + "node_modules/vite/node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha1-LALYZNl/PqbIgwxGTL0Rq26rehw=", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha1-T5RBKoLbMvNuOwuXQfipf+sDH34=", - "license": "ISC", - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, - "node_modules/get-east-asian-width": { - "version": "1.6.0", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", - "integrity": "sha1-IWkA+R3xGossGYw+HZPWwDWndrk=", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha1-dD8OO2lkqTpUke0b/6rgVNf5jQE=", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha1-FQs/J0OGnvPoUewMSdFbHRTQDuE=", - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/glob": { - "version": "13.0.6", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/glob/-/glob-13.0.6.tgz", - "integrity": "sha1-B4ZmVmpCUUfMrPvS4zLetmor5x0=", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "minimatch": "^10.2.2", - "minipass": "^7.1.3", - "path-scurry": "^2.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha1-bSN9mQg5UMeSkPJMdkKj3poo+eM=", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/globals": { - "version": "14.0.0", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/globals/-/globals-14.0.0.tgz", - "integrity": "sha1-iY10E8Kbq89rr+Vvyt3thYrack4=", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha1-ifVrghe9vIgCvSmd9tfxCB1+UaE=", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha1-lEdx/ZyByBJlxNaUGGDaBrtZR5s=", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha1-/JxqeDoISVHQuXH+EBjegTcHozg=", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.4", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/hasown/-/hasown-2.0.4.tgz", - "integrity": "sha1-jGLYy5C+sqrV0KW2dYGtmFTD8AM=", - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/hast-util-from-dom": { - "version": "5.0.1", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/hast-util-from-dom/-/hast-util-from-dom-5.0.1.tgz", - "integrity": "sha1-w8kvvY1OHBYl7es6dzlSueStZKg=", - "license": "ISC", - "dependencies": { - "@types/hast": "^3.0.0", - "hastscript": "^9.0.0", - "web-namespaces": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hast-util-from-html": { - "version": "2.0.3", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/hast-util-from-html/-/hast-util-from-html-2.0.3.tgz", - "integrity": "sha1-SFx0eFNYvrgMS6Y0YpkxGsTEnII=", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0", - "devlop": "^1.1.0", - "hast-util-from-parse5": "^8.0.0", - "parse5": "^7.0.0", - "vfile": "^6.0.0", - "vfile-message": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hast-util-from-html-isomorphic": { - "version": "2.0.0", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/hast-util-from-html-isomorphic/-/hast-util-from-html-isomorphic-2.0.0.tgz", - "integrity": "sha1-sxuu44aomaJHIyajxWkvKfhtHTw=", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0", - "hast-util-from-dom": "^5.0.0", - "hast-util-from-html": "^2.0.0", - "unist-util-remove-position": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hast-util-from-parse5": { - "version": "8.0.3", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/hast-util-from-parse5/-/hast-util-from-parse5-8.0.3.tgz", - "integrity": "sha1-gwo1Ai//KMP+o2l6mML0zGuDWi4=", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0", - "@types/unist": "^3.0.0", - "devlop": "^1.0.0", - "hastscript": "^9.0.0", - "property-information": "^7.0.0", - "vfile": "^6.0.0", - "vfile-location": "^5.0.0", - "web-namespaces": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hast-util-is-element": { - "version": "3.0.0", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/hast-util-is-element/-/hast-util-is-element-3.0.0.tgz", - "integrity": "sha1-bjGmUywhfltTOEjH5Sydk2nKCTI=", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hast-util-parse-selector": { - "version": "4.0.0", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz", - "integrity": "sha1-NSh5+obiVhYDYDfdiTH7XzTLSic=", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hast-util-to-jsx-runtime": { - "version": "2.3.6", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz", - "integrity": "sha1-/zGJeq5Z9iIy4hWU6sfva2MzPpg=", - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0", - "@types/hast": "^3.0.0", - "@types/unist": "^3.0.0", - "comma-separated-tokens": "^2.0.0", - "devlop": "^1.0.0", - "estree-util-is-identifier-name": "^3.0.0", - "hast-util-whitespace": "^3.0.0", - "mdast-util-mdx-expression": "^2.0.0", - "mdast-util-mdx-jsx": "^3.0.0", - "mdast-util-mdxjs-esm": "^2.0.0", - "property-information": "^7.0.0", - "space-separated-tokens": "^2.0.0", - "style-to-js": "^1.0.0", - "unist-util-position": "^5.0.0", - "vfile-message": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hast-util-to-text": { - "version": "4.0.2", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/hast-util-to-text/-/hast-util-to-text-4.0.2.tgz", - "integrity": "sha1-V7Z2kx5xv5y4UkU2eElbMIC/rj4=", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0", - "@types/unist": "^3.0.0", - "hast-util-is-element": "^3.0.0", - "unist-util-find-after": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hast-util-whitespace": { - "version": "3.0.0", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", - "integrity": "sha1-d3jtnTyS3Z6MXI9kiknCH8UctiE=", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hastscript": { - "version": "9.0.1", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/hastscript/-/hastscript-9.0.1.tgz", - "integrity": "sha1-28hL72BR1ACENCwinEUc2dxWff8=", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0", - "comma-separated-tokens": "^2.0.0", - "hast-util-parse-selector": "^4.0.0", - "property-information": "^7.0.0", - "space-separated-tokens": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/highlight.js": { - "version": "10.7.3", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/highlight.js/-/highlight.js-10.7.3.tgz", - "integrity": "sha1-aXJy45kTVuQMPKxWanTu9oF1ZTE=", - "license": "BSD-3-Clause", - "engines": { - "node": "*" - } - }, - "node_modules/highlightjs-vue": { - "version": "1.0.0", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/highlightjs-vue/-/highlightjs-vue-1.0.0.tgz", - "integrity": "sha1-/f6X++pjVOcO5E46lVh14RTbCG0=", - "license": "CC0-1.0" - }, - "node_modules/hono": { - "version": "4.12.30", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/hono/-/hono-4.12.30.tgz", - "integrity": "sha1-TVewUcZmF7ADt7oxiRDLW3nEx6w=", - "license": "MIT", - "engines": { - "node": ">=16.9.0" - } - }, - "node_modules/html-parse-stringify": { - "version": "3.0.1", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", - "integrity": "sha1-38EBc0fOn3fIFBpQfyMwQMWcVdI=", - "license": "MIT", - "dependencies": { - "void-elements": "3.1.0" - } - }, - "node_modules/html-url-attributes": { - "version": "3.0.1", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/html-url-attributes/-/html-url-attributes-3.0.1.tgz", - "integrity": "sha1-g7BSzV5DcHG3Vs10rnD3CIcMLYc=", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/http-errors": { - "version": "2.0.1", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/http-errors/-/http-errors-2.0.1.tgz", - "integrity": "sha1-NtL2W8kJyHkAGN02+02T2myq4Gs=", - "license": "MIT", - "dependencies": { - "depd": "~2.0.0", - "inherits": "~2.0.4", - "setprototypeof": "~1.2.0", - "statuses": "~2.0.2", - "toidentifier": "~1.0.1" - }, - "engines": { - "node": ">= 0.8" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/i18next": { - "version": "26.3.6", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/i18next/-/i18next-26.3.6.tgz", - "integrity": "sha1-WZ2ydcULZtKKadj2xRYsJFzpKWs=", - "funding": [ - { - "type": "individual", - "url": "https://www.locize.com/i18next" - }, - { - "type": "individual", - "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" - }, - { - "type": "individual", - "url": "https://www.locize.com" - } - ], - "license": "MIT", - "peerDependencies": { - "typescript": "^5 || ^6 || ^7" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/iconv-lite": { - "version": "0.7.3", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/iconv-lite/-/iconv-lite-0.7.3.tgz", - "integrity": "sha1-hO4S+WPn3lC8AaE+FgoHizsPQV8=", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/ignore": { - "version": "7.0.6", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/ignore/-/ignore-7.0.6.tgz", - "integrity": "sha1-aleq70yQ3yesNZCHXSno8RmIyI4=", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/import-fresh": { - "version": "3.3.1", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/import-fresh/-/import-fresh-3.3.1.tgz", - "integrity": "sha1-nOy1ZQPAraHydB271lRuSxO1fM8=", - "dev": true, - "license": "MIT", - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/import-fresh/node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha1-SrzYUq0y3Xuqv+m0DgCjbbXzkuY=", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha1-D6LGT5MpF8NDOg3tVTY6rjdBa3w=", - "license": "ISC" - }, - "node_modules/inline-style-parser": { - "version": "0.2.7", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/inline-style-parser/-/inline-style-parser-0.2.7.tgz", - "integrity": "sha1-sfxov8AxO4aFdF5EZON/k3a5yQk=", - "license": "MIT" - }, - "node_modules/internmap": { - "version": "2.0.3", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/internmap/-/internmap-2.0.3.tgz", - "integrity": "sha1-ZoXyN1XkPFJOJR0py8lySOMGEAk=", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/ip-address": { - "version": "10.2.0", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/ip-address/-/ip-address-10.2.0.tgz", - "integrity": "sha1-gF/BeLIMUYvUyFSLJP4wiS1/MgY=", - "license": "MIT", - "engines": { - "node": ">= 12" - } - }, - "node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha1-v/OFQ+64mEglB5/zoqjmy9RngbM=", - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/is-alphabetical": { - "version": "2.0.1", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/is-alphabetical/-/is-alphabetical-2.0.1.tgz", - "integrity": "sha1-AQcgU+p8EDbfPH0Zptqux/GeeJs=", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/is-alphanumerical": { - "version": "2.0.1", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", - "integrity": "sha1-fAP76W4+kxET5X+WSwo2jMLf2HU=", - "license": "MIT", - "dependencies": { - "is-alphabetical": "^2.0.0", - "is-decimal": "^2.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/is-decimal": { - "version": "2.0.1", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/is-decimal/-/is-decimal-2.0.1.tgz", - "integrity": "sha1-lGnS3BkNAhT9h9eLeMrswMwU7vc=", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha1-ZPYeQsu7LuwgcanawLKLoeZdUIQ=", - "dev": true, - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-hexadecimal": { - "version": "2.0.1", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", - "integrity": "sha1-hrW/Zo/KMHSY0xnfwDKJ14GpACc=", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha1-dTU0W4lnNNX4DE0GxQlVUnoU8Ss=", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-plain-obj": { - "version": "4.1.0", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/is-plain-obj/-/is-plain-obj-4.1.0.tgz", - "integrity": "sha1-1lAl7ew2V84DL9fbY8l4g+rtcfA=", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-promise": { - "version": "4.0.0", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/is-promise/-/is-promise-4.0.0.tgz", - "integrity": "sha1-Qv+fhCBsGZHSbev1IN1cAQQt0vM=", - "license": "MIT" - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", - "license": "ISC" - }, - "node_modules/jose": { - "version": "6.2.3", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/jose/-/jose-6.2.3.tgz", - "integrity": "sha1-CXUZetlzJRIhxlijzdxLlRolDC0=", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/panva" - } - }, - "node_modules/joycon": { - "version": "3.1.1", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/joycon/-/joycon-3.1.1.tgz", - "integrity": "sha1-vOhZbWroCPi2gWj1/GkoCZaJTwM=", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha1-GSA/tZmR35jjoocFDUZHzerzJJk=", - "license": "MIT" - }, - "node_modules/js-yaml": { - "version": "4.3.0", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/js-yaml/-/js-yaml-4.3.0.tgz", - "integrity": "sha1-0ZAFcqf3zwtfVAyDZz5gutNDZZI=", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/puzrin" - }, - { - "type": "github", - "url": "https://github.com/sponsors/nodeca" - } - ], - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha1-kziAKjDTtmBfvgYT4JQAjKjAWhM=", - "dev": true, - "license": "MIT" - }, - "node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha1-rnvLNlard6c7pcSb9lTzjmtoYOI=", - "license": "MIT" - }, - "node_modules/json-schema-typed": { - "version": "8.0.2", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/json-schema-typed/-/json-schema-typed-8.0.2.tgz", - "integrity": "sha1-6Y7nsYmf9KGEU00fFnwojGa77/Q=", - "license": "BSD-2-Clause" - }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", - "dev": true, - "license": "MIT" - }, - "node_modules/json-stringify-pretty-compact": { - "version": "4.0.0", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/json-stringify-pretty-compact/-/json-stringify-pretty-compact-4.0.0.tgz", - "integrity": "sha1-z0hEdwvd7jy4mmFw/ksA7uXb8dQ=", - "license": "MIT" - }, - "node_modules/katex": { - "version": "0.17.0", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/katex/-/katex-0.17.0.tgz", - "integrity": "sha1-U24lh07JrIco47S3v3/sZVe32/Q=", - "funding": [ - "https://opencollective.com/katex", - "https://github.com/sponsors/katex" - ], - "license": "MIT", - "dependencies": { - "commander": "^8.3.0" - }, - "bin": { - "katex": "cli.js" - } - }, - "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha1-qHmpnilFL5QkOfKkBeOvizHU3pM=", - "dev": true, - "license": "MIT", - "dependencies": { - "json-buffer": "3.0.1" - } - }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/levn/-/levn-0.4.1.tgz", - "integrity": "sha1-rkViwAdHO5MqYgDUAyaN0v/8at4=", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/lightningcss": { - "version": "1.32.0", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/lightningcss/-/lightningcss-1.32.0.tgz", - "integrity": "sha1-uFqulkhtyxv0mnyFcSISc/Tx5Kk=", - "dev": true, - "license": "MPL-2.0", - "dependencies": { - "detect-libc": "^2.0.3" - }, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - }, - "optionalDependencies": { - "lightningcss-android-arm64": "1.32.0", - "lightningcss-darwin-arm64": "1.32.0", - "lightningcss-darwin-x64": "1.32.0", - "lightningcss-freebsd-x64": "1.32.0", - "lightningcss-linux-arm-gnueabihf": "1.32.0", - "lightningcss-linux-arm64-gnu": "1.32.0", - "lightningcss-linux-arm64-musl": "1.32.0", - "lightningcss-linux-x64-gnu": "1.32.0", - "lightningcss-linux-x64-musl": "1.32.0", - "lightningcss-win32-arm64-msvc": "1.32.0", - "lightningcss-win32-x64-msvc": "1.32.0" - } - }, - "node_modules/lightningcss-android-arm64": { - "version": "1.32.0", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", - "integrity": "sha1-8DOIURbf79nG9UeHUj41FLYeGWg=", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "android" - ], - "peer": true, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-darwin-arm64": { - "version": "1.32.0", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", - "integrity": "sha1-ULcYcbAcgZlYS2SeKSVH+up6+bU=", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "peer": true, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-darwin-x64": { - "version": "1.32.0", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", - "integrity": "sha1-NfPpczLRMLnKGB4RtWje1q68bV4=", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "peer": true, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-freebsd-x64": { - "version": "1.32.0", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", - "integrity": "sha1-l3enZHK2Ttb/lDQq1kx7r9eUpXU=", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "freebsd" - ], - "peer": true, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm-gnueabihf": { - "version": "1.32.0", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", - "integrity": "sha1-E65lLhq3O5E117faFy9mbEEK1T0=", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm64-gnu": { - "version": "1.32.0", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", - "integrity": "sha1-QXhYeVqUWS9oASOhsfnaig4e8zU=", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm64-musl": { - "version": "1.32.0", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", - "integrity": "sha1-a+NmkugQtxgECAL9gJYjz/5zITM=", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-x64-gnu": { - "version": "1.32.0", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", - "integrity": "sha1-C3gDr06yHP043Tn+Kru1PH3QkfY=", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-x64-musl": { - "version": "1.32.0", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", - "integrity": "sha1-iNyLqGXd3bGsXvBLDxYYBEGMFjs=", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-win32-arm64-msvc": { - "version": "1.32.0", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", - "integrity": "sha1-TzC6P6XpJfW3n5RejMDRdsOxqzg=", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "peer": true, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-win32-x64-msvc": { - "version": "1.32.0", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", - "integrity": "sha1-FBqlYFZFBkkokCu0rwRfp9n0Igo=", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "peer": true, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lilconfig": { - "version": "3.1.3", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/lilconfig/-/lilconfig-3.1.3.tgz", - "integrity": "sha1-obz9Ylf5WFv1rhTO7rt7VZAl5MQ=", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antonk52" - } - }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha1-7KKE910pZQeTCdwK2SVauy68FjI=", - "dev": true, - "license": "MIT" - }, - "node_modules/load-tsconfig": { - "version": "0.2.5", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/load-tsconfig/-/load-tsconfig-0.2.5.tgz", - "integrity": "sha1-RTuM2JYb+5Et6nfrbBaP6Myj06E=", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - } - }, - "node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha1-VTIeswn+u8WcSAHZMackUqaB0oY=", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha1-VYqlO0O2YeGSWgr9+japoQhf5Xo=", - "dev": true, - "license": "MIT" - }, - "node_modules/longest-streak": { - "version": "3.1.0", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/longest-streak/-/longest-streak-3.1.0.tgz", - "integrity": "sha1-YvpnzZWHQqFXSvnzmGY2QQLZDNQ=", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/loose-envify": { - "version": "1.4.0", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha1-ce5R+nvkyuwaY4OffmgtgTLTDK8=", - "license": "MIT", - "dependencies": { - "js-tokens": "^3.0.0 || ^4.0.0" - }, - "bin": { - "loose-envify": "cli.js" - } - }, - "node_modules/lowlight": { - "version": "1.20.0", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/lowlight/-/lowlight-1.20.0.tgz", - "integrity": "sha1-3bGX0zRirQ2TvxnRe2wwGqOUGIg=", - "license": "MIT", - "dependencies": { - "fault": "^1.0.0", - "highlight.js": "~10.7.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/lru-cache": { - "version": "11.5.2", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/lru-cache/-/lru-cache-11.5.2.tgz", - "integrity": "sha1-AOFmZckMYg+6FKPDaHMql2ST92A=", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/magic-string": { - "version": "0.30.21", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/magic-string/-/magic-string-0.30.21.tgz", - "integrity": "sha1-VnY+wJoPqAkd8nh5/ZTRkHjADZE=", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.5" - } - }, - "node_modules/markdown-table": { - "version": "3.0.4", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/markdown-table/-/markdown-table-3.0.4.tgz", - "integrity": "sha1-/kTW1BD/nW8uoXl6P2CqTStjHCo=", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha1-oN10voHiqlwvJ+Zc4oNgXuTit/k=", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/mdast-util-find-and-replace": { - "version": "3.0.2", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz", - "integrity": "sha1-cKMXTIlOFN9yKr9DvCUMuuRLEd8=", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "escape-string-regexp": "^5.0.0", - "unist-util-is": "^6.0.0", - "unist-util-visit-parents": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-from-markdown": { - "version": "2.0.3", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.3.tgz", - "integrity": "sha1-yVgiuRqrdfGKTL6LL1G4c+0s8Mc=", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "@types/unist": "^3.0.0", - "decode-named-character-reference": "^1.0.0", - "devlop": "^1.0.0", - "mdast-util-to-string": "^4.0.0", - "micromark": "^4.0.0", - "micromark-util-decode-numeric-character-reference": "^2.0.0", - "micromark-util-decode-string": "^2.0.0", - "micromark-util-normalize-identifier": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0", - "unist-util-stringify-position": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-gfm": { - "version": "3.1.0", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz", - "integrity": "sha1-LN9juSwqMxQGsPsNtMB3wbAzF1E=", - "license": "MIT", - "dependencies": { - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-gfm-autolink-literal": "^2.0.0", - "mdast-util-gfm-footnote": "^2.0.0", - "mdast-util-gfm-strikethrough": "^2.0.0", - "mdast-util-gfm-table": "^2.0.0", - "mdast-util-gfm-task-list-item": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-gfm-autolink-literal": { - "version": "2.0.1", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz", - "integrity": "sha1-q9VXYwM3vTCm1aS9glLhwtwIddU=", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "ccount": "^2.0.0", - "devlop": "^1.0.0", - "mdast-util-find-and-replace": "^3.0.0", - "micromark-util-character": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-gfm-footnote": { - "version": "2.1.0", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz", - "integrity": "sha1-d3jp2co99yOMwr0/orG/amWxlAM=", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "devlop": "^1.1.0", - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0", - "micromark-util-normalize-identifier": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-gfm-strikethrough": { - "version": "2.0.0", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz", - "integrity": "sha1-1E756O0oOsjBFlqw0N/QWMJ2TBY=", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-gfm-table": { - "version": "2.0.0", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz", - "integrity": "sha1-ekNftiI6crCGKzOvvXErba6HjTg=", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "devlop": "^1.0.0", - "markdown-table": "^3.0.0", - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-gfm-task-list-item": { - "version": "2.0.0", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz", - "integrity": "sha1-5oCV0vikMD7yQJSrZC4QR7mRqTY=", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "devlop": "^1.0.0", - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-math": { - "version": "3.0.0", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/mdast-util-math/-/mdast-util-math-3.0.0.tgz", - "integrity": "sha1-jXndO6+KuKx4H2K4hTdoGQuaALA=", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0", - "@types/mdast": "^4.0.0", - "devlop": "^1.0.0", - "longest-streak": "^3.0.0", - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.1.0", - "unist-util-remove-position": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-mdx-expression": { - "version": "2.0.1", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz", - "integrity": "sha1-Q/CrrJrcdW4ghvY4IqOMjTw6UJY=", - "license": "MIT", - "dependencies": { - "@types/estree-jsx": "^1.0.0", - "@types/hast": "^3.0.0", - "@types/mdast": "^4.0.0", - "devlop": "^1.0.0", - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-mdx-jsx": { - "version": "3.2.0", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz", - "integrity": "sha1-/QTGeip0me+5BailxXjd3J/a2g0=", - "license": "MIT", - "dependencies": { - "@types/estree-jsx": "^1.0.0", - "@types/hast": "^3.0.0", - "@types/mdast": "^4.0.0", - "@types/unist": "^3.0.0", - "ccount": "^2.0.0", - "devlop": "^1.1.0", - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0", - "parse-entities": "^4.0.0", - "stringify-entities": "^4.0.0", - "unist-util-stringify-position": "^4.0.0", - "vfile-message": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-mdxjs-esm": { - "version": "2.0.1", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz", - "integrity": "sha1-AZz751etYt1VfbNaaV5zFLzJ+pc=", - "license": "MIT", - "dependencies": { - "@types/estree-jsx": "^1.0.0", - "@types/hast": "^3.0.0", - "@types/mdast": "^4.0.0", - "devlop": "^1.0.0", - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-phrasing": { - "version": "4.1.0", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", - "integrity": "sha1-fMCo3sMOrwS3salmGpKtszgqpuM=", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "unist-util-is": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-to-hast": { - "version": "13.2.1", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", - "integrity": "sha1-1/+EykmaV+LAYK5nVIrZUOaJoFM=", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0", - "@types/mdast": "^4.0.0", - "@ungap/structured-clone": "^1.0.0", - "devlop": "^1.0.0", - "micromark-util-sanitize-uri": "^2.0.0", - "trim-lines": "^3.0.0", - "unist-util-position": "^5.0.0", - "unist-util-visit": "^5.0.0", - "vfile": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-to-markdown": { - "version": "2.1.2", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz", - "integrity": "sha1-+RD/5giX8Eu0t+fuQ0SG92KINhs=", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "@types/unist": "^3.0.0", - "longest-streak": "^3.0.0", - "mdast-util-phrasing": "^4.0.0", - "mdast-util-to-string": "^4.0.0", - "micromark-util-classify-character": "^2.0.0", - "micromark-util-decode-string": "^2.0.0", - "unist-util-visit": "^5.0.0", - "zwitch": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-to-string": { - "version": "4.0.0", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", - "integrity": "sha1-elEhR1VWoE5+3etnsmSq550xKBQ=", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/media-typer": { - "version": "1.1.0", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/media-typer/-/media-typer-1.1.0.tgz", - "integrity": "sha1-ardLjy0zIPIGSyqHo455Mf86VWE=", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/merge-descriptors": { - "version": "2.0.0", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/merge-descriptors/-/merge-descriptors-2.0.0.tgz", - "integrity": "sha1-6pIvZgY1oiSe5WXgRJ+VHmtgOAg=", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/micromark": { - "version": "4.0.2", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/micromark/-/micromark-4.0.2.tgz", - "integrity": "sha1-kTlaPhiEoZjmIRbjPJxWjjmTb9s=", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "@types/debug": "^4.0.0", - "debug": "^4.0.0", - "decode-named-character-reference": "^1.0.0", - "devlop": "^1.0.0", - "micromark-core-commonmark": "^2.0.0", - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-chunked": "^2.0.0", - "micromark-util-combine-extensions": "^2.0.0", - "micromark-util-decode-numeric-character-reference": "^2.0.0", - "micromark-util-encode": "^2.0.0", - "micromark-util-normalize-identifier": "^2.0.0", - "micromark-util-resolve-all": "^2.0.0", - "micromark-util-sanitize-uri": "^2.0.0", - "micromark-util-subtokenize": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-core-commonmark": { - "version": "2.0.3", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", - "integrity": "sha1-xpFjDkhQIaaM8o28Kyyifr9njNQ=", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "decode-named-character-reference": "^1.0.0", - "devlop": "^1.0.0", - "micromark-factory-destination": "^2.0.0", - "micromark-factory-label": "^2.0.0", - "micromark-factory-space": "^2.0.0", - "micromark-factory-title": "^2.0.0", - "micromark-factory-whitespace": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-chunked": "^2.0.0", - "micromark-util-classify-character": "^2.0.0", - "micromark-util-html-tag-name": "^2.0.0", - "micromark-util-normalize-identifier": "^2.0.0", - "micromark-util-resolve-all": "^2.0.0", - "micromark-util-subtokenize": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-extension-gfm": { - "version": "3.0.0", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz", - "integrity": "sha1-PhM3arld16XP0OKVYN/pmWV7PFs=", - "license": "MIT", - "dependencies": { - "micromark-extension-gfm-autolink-literal": "^2.0.0", - "micromark-extension-gfm-footnote": "^2.0.0", - "micromark-extension-gfm-strikethrough": "^2.0.0", - "micromark-extension-gfm-table": "^2.0.0", - "micromark-extension-gfm-tagfilter": "^2.0.0", - "micromark-extension-gfm-task-list-item": "^2.0.0", - "micromark-util-combine-extensions": "^2.0.0", - "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-gfm-autolink-literal": { - "version": "2.1.0", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz", - "integrity": "sha1-Yoau6WhsRGLB41UqnVBf7dzuuTU=", - "license": "MIT", - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-sanitize-uri": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-gfm-footnote": { - "version": "2.1.0", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz", - "integrity": "sha1-TatW1OOYuYU/b+TvrE/JNh8+B1A=", - "license": "MIT", - "dependencies": { - "devlop": "^1.0.0", - "micromark-core-commonmark": "^2.0.0", - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-normalize-identifier": "^2.0.0", - "micromark-util-sanitize-uri": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-gfm-strikethrough": { - "version": "2.1.0", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz", - "integrity": "sha1-hhBt+LOmkrX2qSKA04eb5r5G2SM=", - "license": "MIT", - "dependencies": { - "devlop": "^1.0.0", - "micromark-util-chunked": "^2.0.0", - "micromark-util-classify-character": "^2.0.0", - "micromark-util-resolve-all": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-gfm-table": { - "version": "2.1.1", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz", - "integrity": "sha1-+scLy/Uf5l9fRAMxGNOb6Km1lAs=", - "license": "MIT", - "dependencies": { - "devlop": "^1.0.0", - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-gfm-tagfilter": { - "version": "2.0.0", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz", - "integrity": "sha1-8m2KeAe1mF+6E89hRltYyl/33Fc=", - "license": "MIT", - "dependencies": { - "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-gfm-task-list-item": { - "version": "2.1.0", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz", - "integrity": "sha1-vMNNgFY5gpmQ7BdcPuoSu1t4Hyw=", - "license": "MIT", - "dependencies": { - "devlop": "^1.0.0", - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-math": { - "version": "3.1.0", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/micromark-extension-math/-/micromark-extension-math-3.1.0.tgz", - "integrity": "sha1-xC7jsd1amgNYToPdjwjj3lECEsE=", - "license": "MIT", - "dependencies": { - "@types/katex": "^0.16.0", - "devlop": "^1.0.0", - "katex": "^0.16.0", - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-math/node_modules/katex": { - "version": "0.16.47", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/katex/-/katex-0.16.47.tgz", - "integrity": "sha1-ChOkLC3rT3TmHxYtRAuRZaVIAw8=", - "funding": [ - "https://opencollective.com/katex", - "https://github.com/sponsors/katex" - ], - "license": "MIT", - "dependencies": { - "commander": "^8.3.0" - }, - "bin": { - "katex": "cli.js" - } - }, - "node_modules/micromark-factory-destination": { - "version": "2.0.1", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", - "integrity": "sha1-j++OD3CB8EdPvdkt61DJkKAmRjk=", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-factory-label": { - "version": "2.0.1", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", - "integrity": "sha1-UmfvqX8eUlTvx/ILRZo4yyEFi6E=", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "devlop": "^1.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-factory-space": { - "version": "2.0.1", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", - "integrity": "sha1-NtAhLpYrKzEh+FJfx6PHwCnzNPw=", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-factory-title": { - "version": "2.0.1", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", - "integrity": "sha1-I35KpdWKlYY/AQMtnumwkPHebpQ=", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-factory-whitespace": { - "version": "2.0.1", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", - "integrity": "sha1-BrJrKYPE0nv8xlezPiUTTUhosLE=", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-character": { - "version": "2.1.1", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/micromark-util-character/-/micromark-util-character-2.1.1.tgz", - "integrity": "sha1-L5h4MaQNTFEKwmHomFLE6XA8zaY=", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-chunked": { - "version": "2.0.1", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", - "integrity": "sha1-R/vNk0caP8yrhs/wOEf8NVLbEFE=", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^2.0.0" - } - }, - "node_modules/micromark-util-classify-character": { - "version": "2.0.1", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", - "integrity": "sha1-05n6+cRcoUyLS+mLHqSBvO2Htik=", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-combine-extensions": { - "version": "2.0.1", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", - "integrity": "sha1-Kg9JCrCL/1zC/V7sbdDKBPibMKk=", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-chunked": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-decode-numeric-character-reference": { - "version": "2.0.2", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", - "integrity": "sha1-/PFbZgl5OI5vEYzba/fXnXPSb+U=", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^2.0.0" - } - }, - "node_modules/micromark-util-decode-string": { - "version": "2.0.1", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", - "integrity": "sha1-bLmVguXScehO/KjmGoB5lNcWHrI=", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "decode-named-character-reference": "^1.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-decode-numeric-character-reference": "^2.0.0", - "micromark-util-symbol": "^2.0.0" - } - }, - "node_modules/micromark-util-encode": { - "version": "2.0.1", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", - "integrity": "sha1-DVHRwJVVHPqsNoMmljz1XxX1QLg=", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark-util-html-tag-name": { - "version": "2.0.1", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", - "integrity": "sha1-5AQDCWSBmGtBwQZif5j3LU0QuCU=", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark-util-normalize-identifier": { - "version": "2.0.1", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", - "integrity": "sha1-ww13sugyrPZSb4vxqke8nJQ4wW0=", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^2.0.0" - } - }, - "node_modules/micromark-util-resolve-all": { - "version": "2.0.1", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", - "integrity": "sha1-4aLWLN0jcjCirhGDkCexk4HjHos=", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-sanitize-uri": { - "version": "2.0.1", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", - "integrity": "sha1-q4l4m4GKWHUrc9a1UjhiG3+qj9c=", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-encode": "^2.0.0", - "micromark-util-symbol": "^2.0.0" - } - }, - "node_modules/micromark-util-subtokenize": { - "version": "2.1.0", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", - "integrity": "sha1-2K3lug8xl6HPaimZ+7/mNXoaGe4=", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "devlop": "^1.0.0", - "micromark-util-chunked": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-symbol": { - "version": "2.0.1", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", - "integrity": "sha1-5dpJTo6ysHGg0I+zT2zv7GwKGbg=", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark-util-types": { - "version": "2.0.2", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/micromark-util-types/-/micromark-util-types-2.0.2.tgz", - "integrity": "sha1-8AIl9fWg68MlT5bDa2YFxLOTkI4=", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha1-1m+hjzpHB2eJMgubGvMr2G2fogI=", - "dev": true, - "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/micromatch/node_modules/picomatch": { - "version": "2.3.2", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/picomatch/-/picomatch-2.3.2.tgz", - "integrity": "sha1-WpQpFeJrNy3A8OZ1MUmhbmscVgE=", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha1-zds+5PnGRTDf9kAjZmHULLajFPU=", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "3.0.2", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/mime-types/-/mime-types-3.0.2.tgz", - "integrity": "sha1-OQAtQYJXXVrwNv+hGBAPJSSy4qs=", - "license": "MIT", - "dependencies": { - "mime-db": "^1.54.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/minimatch": { - "version": "10.2.5", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/minimatch/-/minimatch-10.2.5.tgz", - "integrity": "sha1-vUhoegvjjtKWE5kQVgD4MglYYdE=", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "brace-expansion": "^5.0.5" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/minipass": { - "version": "7.1.3", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/minipass/-/minipass-7.1.3.tgz", - "integrity": "sha1-eTibTrG7LQA6m7qH1JLyvTe9xls=", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/mlly": { - "version": "1.8.2", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/mlly/-/mlly-1.8.2.tgz", - "integrity": "sha1-5/eRmoLROxdEBWExFySaP0SdeLs=", - "dev": true, - "license": "MIT", - "dependencies": { - "acorn": "^8.16.0", - "pathe": "^2.0.3", - "pkg-types": "^1.3.1", - "ufo": "^1.6.3" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/ms/-/ms-2.1.3.tgz", - "integrity": "sha1-V0yBOM4dK1hh8LRFedut1gxmFbI=", - "license": "MIT" - }, - "node_modules/mz": { - "version": "2.7.0", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/mz/-/mz-2.7.0.tgz", - "integrity": "sha1-lQCAV6Vsr63CvGPd5/n/aVWUjjI=", - "dev": true, - "license": "MIT", - "dependencies": { - "any-promise": "^1.0.0", - "object-assign": "^4.0.1", - "thenify-all": "^1.0.0" - } - }, - "node_modules/nanoid": { - "version": "3.3.16", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/nanoid/-/nanoid-3.3.16.tgz", - "integrity": "sha1-oE2OxLHxAAnS1TOUeu/kKTc3gWw=", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", - "dev": true, - "license": "MIT" - }, - "node_modules/negotiator": { - "version": "1.0.0", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/negotiator/-/negotiator-1.0.0.tgz", - "integrity": "sha1-tskbtHFy1p+Tz9fDV7u1KQGbX2o=", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-inspect": { - "version": "1.13.4", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/object-inspect/-/object-inspect-1.13.4.tgz", - "integrity": "sha1-g3UmXiG8IND6WCwi4bE0hdbgAhM=", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/obug": { - "version": "2.1.4", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/obug/-/obug-2.1.4.tgz", - "integrity": "sha1-kJDYpUilIlF5FdKqaq6QcZesbPg=", - "dev": true, - "funding": [ - "https://github.com/sponsors/sxzz", - "https://opencollective.com/debug" - ], - "license": "MIT", - "engines": { - "node": ">=12.20.0" - } - }, - "node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha1-WMjEQRblSEWtV/FKsQsDUzGErD8=", - "license": "MIT", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/optionator": { - "version": "0.9.4", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/optionator/-/optionator-0.9.4.tgz", - "integrity": "sha1-fqHBpdkddk+yghOciP4R4YKjpzQ=", - "dev": true, - "license": "MIT", - "dependencies": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.5" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha1-4drMvnjQ0TiMoYxk/qOOPlfjcGs=", - "dev": true, - "license": "MIT", - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha1-g8gxXGeFAF470CGDlBHJ4RDm2DQ=", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/package-json-from-dist": { - "version": "1.0.1", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", - "integrity": "sha1-TxRxoBCCeob5TP2bByfjbSZ95QU=", - "dev": true, - "license": "BlueOak-1.0.0" - }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha1-aR0nCeeMefrjoVZiJFLQB2LKqqI=", - "dev": true, - "license": "MIT", - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/parse-entities": { - "version": "4.0.2", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/parse-entities/-/parse-entities-4.0.2.tgz", - "integrity": "sha1-YdRvXtKOTuYundxD1rAQGIRD8Vk=", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "character-entities-legacy": "^3.0.0", - "character-reference-invalid": "^2.0.0", - "decode-named-character-reference": "^1.0.0", - "is-alphanumerical": "^2.0.0", - "is-decimal": "^2.0.0", - "is-hexadecimal": "^2.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/parse-entities/node_modules/@types/unist": { - "version": "2.0.11", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@types/unist/-/unist-2.0.11.tgz", - "integrity": "sha1-Ea9XsSfjJId3SEH3pOVOqxZtA8Q=", - "license": "MIT" - }, - "node_modules/parse5": { - "version": "7.3.0", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/parse5/-/parse5-7.3.0.tgz", - "integrity": "sha1-1+Ik+nI5nHoXUJn0X8KtAksF7AU=", - "license": "MIT", - "dependencies": { - "entities": "^6.0.0" - }, - "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" - } - }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha1-naGee+6NEt/wUT7Vt2lXeTvC6NQ=", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha1-UTvb4tO5XXdi6METfvoZXGxhtbM=", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha1-WB9q3mWMu6ZaDTOA3ndTKVBU83U=", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-scurry": { - "version": "2.0.2", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/path-scurry/-/path-scurry-2.0.2.tgz", - "integrity": "sha1-a+DQ7gKhDZ4N56mLrmXhgskGH4U=", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^11.0.0", - "minipass": "^7.1.2" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/path-to-regexp": { - "version": "8.4.2", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/path-to-regexp/-/path-to-regexp-8.4.2.tgz", - "integrity": "sha1-eVxCDE98pFxbiHNm9iLuDJhSzM0=", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/pathe": { - "version": "2.0.3", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/pathe/-/pathe-2.0.3.tgz", - "integrity": "sha1-PsvsVUIWhbcKnahyss/z4cvtFxY=", - "dev": true, - "license": "MIT" - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha1-PTIa8+q5ObCDyPkpodEs2oHCa2s=", - "dev": true, - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "4.0.5", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/picomatch/-/picomatch-4.0.5.tgz", - "integrity": "sha1-UepXoX2G9gX4EDlZX7xA7QalX6s=", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/pirates": { - "version": "4.0.7", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/pirates/-/pirates-4.0.7.tgz", - "integrity": "sha1-ZDtKGMQlfIplEEtz8wSc6aChXiI=", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/pkce-challenge": { - "version": "5.0.1", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/pkce-challenge/-/pkce-challenge-5.0.1.tgz", - "integrity": "sha1-O0RGhlsXsXRems4gFqMfSN32Iw0=", - "license": "MIT", - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/pkg-types": { - "version": "1.3.1", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/pkg-types/-/pkg-types-1.3.1.tgz", - "integrity": "sha1-vXzHCIEZJ3fu9TJsGd60bokJF98=", - "dev": true, - "license": "MIT", - "dependencies": { - "confbox": "^0.1.8", - "mlly": "^1.7.4", - "pathe": "^2.0.1" - } - }, - "node_modules/plotly.js-dist-min": { - "version": "2.35.3", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/plotly.js-dist-min/-/plotly.js-dist-min-2.35.3.tgz", - "integrity": "sha1-uAx5VG/iHB9YVle1nYDK8aDY9WM=", - "license": "MIT" - }, - "node_modules/postcss": { - "version": "8.5.19", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/postcss/-/postcss-8.5.19.tgz", - "integrity": "sha1-Ra1c/eSZQI4gFHNII3VROBqSIDc=", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.12", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/postcss-load-config": { - "version": "6.0.1", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/postcss-load-config/-/postcss-load-config-6.0.1.tgz", - "integrity": "sha1-b9fc2K6JutzxstZESJy6v4OqgJY=", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "lilconfig": "^3.1.1" - }, - "engines": { - "node": ">= 18" - }, - "peerDependencies": { - "jiti": ">=1.21.0", - "postcss": ">=8.0.9", - "tsx": "^4.8.1", - "yaml": "^2.4.2" - }, - "peerDependenciesMeta": { - "jiti": { - "optional": true - }, - "postcss": { - "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true - } - } - }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha1-3rxkidem5rDnYRiIzsiAM30xY5Y=", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/prettier": { - "version": "3.9.5", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/prettier/-/prettier-3.9.5.tgz", - "integrity": "sha1-T+yXc24zudC2ILSJFP6TtTDoNa0=", - "dev": true, - "license": "MIT", - "bin": { - "prettier": "bin/prettier.cjs" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" - } - }, - "node_modules/prismjs": { - "version": "1.30.0", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/prismjs/-/prismjs-1.30.0.tgz", - "integrity": "sha1-2XCZadnU4WQD9vNIxjVTsZ8Jdak=", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/property-information": { - "version": "7.2.0", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/property-information/-/property-information-7.2.0.tgz", - "integrity": "sha1-CAmzQmTplcC/zTInAooeNSEK+Ao=", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha1-8Z/mnOqzEe65S0LnDowgcPm6ECU=", - "license": "MIT", - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha1-AnQi4vrsCyXhVJw+G9gwm5EztuU=", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/qs": { - "version": "6.15.3", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/qs/-/qs-6.15.3.tgz", - "integrity": "sha1-doUhMqWO1cfA72fkRBubtdYGGzs=", - "license": "BSD-3-Clause", - "dependencies": { - "es-define-property": "^1.0.1", - "side-channel": "^1.1.1" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/range-parser": { - "version": "1.3.0", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/range-parser/-/range-parser-1.3.0.tgz", - "integrity": "sha1-1/Gb6BK7YnIUcrRdO+IZ7wlXK0c=", - "license": "MIT", - "engines": { - "node": ">= 0.6" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/raw-body": { - "version": "3.0.2", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/raw-body/-/raw-body-3.0.2.tgz", - "integrity": "sha1-PjraWuVWj5CV2EN2/TpJuPsAClE=", - "license": "MIT", - "dependencies": { - "bytes": "~3.1.2", - "http-errors": "~2.0.1", - "iconv-lite": "~0.7.0", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/react": { - "version": "18.3.1", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/react/-/react-18.3.1.tgz", - "integrity": "sha1-SauJIAnFOTNiW9FrJTP8dUyrKJE=", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-dom": { - "version": "18.3.1", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/react-dom/-/react-dom-18.3.1.tgz", - "integrity": "sha1-wiZdeVEbV9R5s90/36UVNklMXLQ=", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0", - "scheduler": "^0.23.2" - }, - "peerDependencies": { - "react": "^18.3.1" - } - }, - "node_modules/react-i18next": { - "version": "17.0.10", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/react-i18next/-/react-i18next-17.0.10.tgz", - "integrity": "sha1-rv30MkdIyxBc/vke00fqsSmGVpg=", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.29.2", - "html-parse-stringify": "^3.0.1", - "use-sync-external-store": "^1.6.0" - }, - "peerDependencies": { - "i18next": ">= 26.2.0", - "react": ">= 16.8.0", - "typescript": "^5 || ^6 || ^7" - }, - "peerDependenciesMeta": { - "react-dom": { - "optional": true - }, - "react-native": { - "optional": true - }, - "typescript": { - "optional": true - } - } - }, - "node_modules/react-markdown": { - "version": "10.1.0", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/react-markdown/-/react-markdown-10.1.0.tgz", - "integrity": "sha1-4ivCD63bwHYFwVKEJVZTwPO61co=", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0", - "@types/mdast": "^4.0.0", - "devlop": "^1.0.0", - "hast-util-to-jsx-runtime": "^2.0.0", - "html-url-attributes": "^3.0.0", - "mdast-util-to-hast": "^13.0.0", - "remark-parse": "^11.0.0", - "remark-rehype": "^11.0.0", - "unified": "^11.0.0", - "unist-util-visit": "^5.0.0", - "vfile": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - }, - "peerDependencies": { - "@types/react": ">=18", - "react": ">=18" - } - }, - "node_modules/react-router": { - "version": "7.18.1", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/react-router/-/react-router-7.18.1.tgz", - "integrity": "sha1-YSWdFZS5XBrOKZ7kxXRTVw8MIvE=", - "license": "MIT", - "dependencies": { - "cookie": "^1.0.1", - "set-cookie-parser": "^2.6.0" - }, - "engines": { - "node": ">=20.0.0" - }, - "peerDependencies": { - "react": ">=18", - "react-dom": ">=18" - }, - "peerDependenciesMeta": { - "react-dom": { - "optional": true - } - } - }, - "node_modules/react-router-dom": { - "version": "7.18.1", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/react-router-dom/-/react-router-dom-7.18.1.tgz", - "integrity": "sha1-DRsTjikTkwWa1IHD4Q42Y4WpeKQ=", - "license": "MIT", - "dependencies": { - "react-router": "7.18.1" - }, - "engines": { - "node": ">=20.0.0" - }, - "peerDependencies": { - "react": ">=18", - "react-dom": ">=18" - } - }, - "node_modules/react-router/node_modules/cookie": { - "version": "1.1.1", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/cookie/-/cookie-1.1.1.tgz", - "integrity": "sha1-O7m9/II2nbnC9pyTycPOsxDIizw=", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/react-syntax-highlighter": { - "version": "16.1.1", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/react-syntax-highlighter/-/react-syntax-highlighter-16.1.1.tgz", - "integrity": "sha1-koRZhV03X1z8jmRgceINVBzry1I=", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.28.4", - "highlight.js": "^10.4.1", - "highlightjs-vue": "^1.0.0", - "lowlight": "^1.17.0", - "prismjs": "^1.30.0", - "refractor": "^5.0.0" - }, - "engines": { - "node": ">= 16.20.2" - }, - "peerDependencies": { - "react": ">= 0.14.0" - } - }, - "node_modules/readdirp": { - "version": "4.1.2", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/readdirp/-/readdirp-4.1.2.tgz", - "integrity": "sha1-64WAFDX78qfuWPGeCSGwaPxplI0=", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14.18.0" - }, - "funding": { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/refractor": { - "version": "5.0.0", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/refractor/-/refractor-5.0.0.tgz", - "integrity": "sha1-hdrwRIptlH9TYXlusiwxcz1h2QQ=", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0", - "@types/prismjs": "^1.0.0", - "hastscript": "^9.0.0", - "parse-entities": "^4.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/rehype-katex": { - "version": "7.0.1", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/rehype-katex/-/rehype-katex-7.0.1.tgz", - "integrity": "sha1-gy5tevJ0SiKJgdGw/olIOp58k6E=", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0", - "@types/katex": "^0.16.0", - "hast-util-from-html-isomorphic": "^2.0.0", - "hast-util-to-text": "^4.0.0", - "katex": "^0.16.0", - "unist-util-visit-parents": "^6.0.0", - "vfile": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/rehype-katex/node_modules/katex": { - "version": "0.16.47", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/katex/-/katex-0.16.47.tgz", - "integrity": "sha1-ChOkLC3rT3TmHxYtRAuRZaVIAw8=", - "funding": [ - "https://opencollective.com/katex", - "https://github.com/sponsors/katex" - ], - "license": "MIT", - "dependencies": { - "commander": "^8.3.0" - }, - "bin": { - "katex": "cli.js" - } - }, - "node_modules/remark-gfm": { - "version": "4.0.1", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/remark-gfm/-/remark-gfm-4.0.1.tgz", - "integrity": "sha1-MyJ7KnQ5dnDTV78FwJjq+FE/DWs=", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "mdast-util-gfm": "^3.0.0", - "micromark-extension-gfm": "^3.0.0", - "remark-parse": "^11.0.0", - "remark-stringify": "^11.0.0", - "unified": "^11.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-math": { - "version": "6.0.0", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/remark-math/-/remark-math-6.0.0.tgz", - "integrity": "sha1-Cs33RnXxwZX+pu//p4WC9+1/wNc=", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "mdast-util-math": "^3.0.0", - "micromark-extension-math": "^3.0.0", - "unified": "^11.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-parse": { - "version": "11.0.0", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/remark-parse/-/remark-parse-11.0.0.tgz", - "integrity": "sha1-qmB0P8s36/awaSBOtNowTkDbRaE=", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "mdast-util-from-markdown": "^2.0.0", - "micromark-util-types": "^2.0.0", - "unified": "^11.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-rehype": { - "version": "11.1.2", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/remark-rehype/-/remark-rehype-11.1.2.tgz", - "integrity": "sha1-Kt2q3agMqb2aoNp2PnTRYydoOzc=", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0", - "@types/mdast": "^4.0.0", - "mdast-util-to-hast": "^13.0.0", - "unified": "^11.0.0", - "vfile": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-stringify": { - "version": "11.0.0", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/remark-stringify/-/remark-stringify-11.0.0.tgz", - "integrity": "sha1-TFsB3XEcJp3xqq4RdD634udjb9M=", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "mdast-util-to-markdown": "^2.0.0", - "unified": "^11.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/require-from-string": { - "version": "2.0.2", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha1-iaf92TgmEmcxjq/hT5wy5ZjDaQk=", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/resolve-from": { - "version": "5.0.0", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha1-w1IlhD3493bfIcV1V7wIfp39/Gk=", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/rimraf": { - "version": "6.1.3", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/rimraf/-/rimraf-6.1.3.tgz", - "integrity": "sha1-r77iNrO9K+Mx1OfORJO6wXGJga8=", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "glob": "^13.0.3", - "package-json-from-dist": "^1.0.1" - }, - "bin": { - "rimraf": "dist/esm/bin.mjs" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/robust-predicates": { - "version": "3.0.3", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/robust-predicates/-/robust-predicates-3.0.3.tgz", - "integrity": "sha1-EJkGGzNJ4sWr7GwqsKzUQNJNQGI=", - "license": "Unlicense" - }, - "node_modules/rolldown": { - "version": "1.1.5", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/rolldown/-/rolldown-1.1.5.tgz", - "integrity": "sha1-M5quJQhENR/FW3TiZS0+vW+6OJ0=", - "dev": true, - "license": "MIT", - "dependencies": { - "@oxc-project/types": "=0.139.0", - "@rolldown/pluginutils": "^1.0.0" - }, - "bin": { - "rolldown": "bin/cli.mjs" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "optionalDependencies": { - "@rolldown/binding-android-arm64": "1.1.5", - "@rolldown/binding-darwin-arm64": "1.1.5", - "@rolldown/binding-darwin-x64": "1.1.5", - "@rolldown/binding-freebsd-x64": "1.1.5", - "@rolldown/binding-linux-arm-gnueabihf": "1.1.5", - "@rolldown/binding-linux-arm64-gnu": "1.1.5", - "@rolldown/binding-linux-arm64-musl": "1.1.5", - "@rolldown/binding-linux-ppc64-gnu": "1.1.5", - "@rolldown/binding-linux-s390x-gnu": "1.1.5", - "@rolldown/binding-linux-x64-gnu": "1.1.5", - "@rolldown/binding-linux-x64-musl": "1.1.5", - "@rolldown/binding-openharmony-arm64": "1.1.5", - "@rolldown/binding-wasm32-wasi": "1.1.5", - "@rolldown/binding-win32-arm64-msvc": "1.1.5", - "@rolldown/binding-win32-x64-msvc": "1.1.5" - } - }, - "node_modules/rollup": { - "version": "4.62.2", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/rollup/-/rollup-4.62.2.tgz", - "integrity": "sha1-2Q/Ey4EfBxMDyJC3eVlWNPNflUE=", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "1.0.9" - }, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.62.2", - "@rollup/rollup-android-arm64": "4.62.2", - "@rollup/rollup-darwin-arm64": "4.62.2", - "@rollup/rollup-darwin-x64": "4.62.2", - "@rollup/rollup-freebsd-arm64": "4.62.2", - "@rollup/rollup-freebsd-x64": "4.62.2", - "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", - "@rollup/rollup-linux-arm-musleabihf": "4.62.2", - "@rollup/rollup-linux-arm64-gnu": "4.62.2", - "@rollup/rollup-linux-arm64-musl": "4.62.2", - "@rollup/rollup-linux-loong64-gnu": "4.62.2", - "@rollup/rollup-linux-loong64-musl": "4.62.2", - "@rollup/rollup-linux-ppc64-gnu": "4.62.2", - "@rollup/rollup-linux-ppc64-musl": "4.62.2", - "@rollup/rollup-linux-riscv64-gnu": "4.62.2", - "@rollup/rollup-linux-riscv64-musl": "4.62.2", - "@rollup/rollup-linux-s390x-gnu": "4.62.2", - "@rollup/rollup-linux-x64-gnu": "4.62.2", - "@rollup/rollup-linux-x64-musl": "4.62.2", - "@rollup/rollup-openbsd-x64": "4.62.2", - "@rollup/rollup-openharmony-arm64": "4.62.2", - "@rollup/rollup-win32-arm64-msvc": "4.62.2", - "@rollup/rollup-win32-ia32-msvc": "4.62.2", - "@rollup/rollup-win32-x64-gnu": "4.62.2", - "@rollup/rollup-win32-x64-msvc": "4.62.2", - "fsevents": "~2.3.2" - } - }, - "node_modules/router": { - "version": "2.2.0", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/router/-/router-2.2.0.tgz", - "integrity": "sha1-AZvmILcRyHZBFnzHm5kJDwCxRu8=", - "license": "MIT", - "dependencies": { - "debug": "^4.4.0", - "depd": "^2.0.0", - "is-promise": "^4.0.0", - "parseurl": "^1.3.3", - "path-to-regexp": "^8.0.0" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/rw": { - "version": "1.3.3", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/rw/-/rw-1.3.3.tgz", - "integrity": "sha1-P4Yt+pGrdmsUiF700BEkv9oHT7Q=", - "license": "BSD-3-Clause" - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha1-RPoWGwGHuVSd2Eu5GAL5vYOFzWo=", - "license": "MIT" - }, - "node_modules/scheduler": { - "version": "0.23.2", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/scheduler/-/scheduler-0.23.2.tgz", - "integrity": "sha1-QUumSjsoKJLpRM8hCOzAeNEVzcM=", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0" - } - }, - "node_modules/semver": { - "version": "7.8.5", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/semver/-/semver-7.8.5.tgz", - "integrity": "sha1-ObZGA33VDBT7RR5+TKxY7YuGP2k=", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/send": { - "version": "1.2.1", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/send/-/send-1.2.1.tgz", - "integrity": "sha1-nqt0O4dPNVD0CiaGe/KGrWDT8+0=", - "license": "MIT", - "dependencies": { - "debug": "^4.4.3", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "fresh": "^2.0.0", - "http-errors": "^2.0.1", - "mime-types": "^3.0.2", - "ms": "^2.1.3", - "on-finished": "^2.4.1", - "range-parser": "^1.2.1", - "statuses": "^2.0.2" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/serve-static": { - "version": "2.2.1", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/serve-static/-/serve-static-2.2.1.tgz", - "integrity": "sha1-fxhqSk5fW2Y616QpT/G/N88OmKk=", - "license": "MIT", - "dependencies": { - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "parseurl": "^1.3.3", - "send": "^1.2.0" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/set-cookie-parser": { - "version": "2.7.2", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", - "integrity": "sha1-zNCGc6muXS5E6iot4lCJ5nx+32g=", - "license": "MIT" - }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha1-ZsmiSnP5/CjL5msJ/tPTPcrxtCQ=", - "license": "ISC" - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha1-zNCvT4g1+9wmW4JGGq8MNmY/NOo=", - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha1-rhbxZE2HPsrYQ7AwexQzYtTEIXI=", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/side-channel": { - "version": "1.1.1", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/side-channel/-/side-channel-1.1.1.tgz", - "integrity": "sha1-6gLGLgXcS+pn1EQvD7ce4ZL44Ks=", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.4", - "side-channel-list": "^1.0.1", - "side-channel-map": "^1.0.1", - "side-channel-weakmap": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-list": { - "version": "1.0.1", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/side-channel-list/-/side-channel-list-1.0.1.tgz", - "integrity": "sha1-wuC1oUpUCuvuO7xsP4ZmzJtQkSc=", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-map": { - "version": "1.0.1", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/side-channel-map/-/side-channel-map-1.0.1.tgz", - "integrity": "sha1-1rtrN5Asb+9RdOX1M/q0xzKib0I=", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-weakmap": { - "version": "1.0.2", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", - "integrity": "sha1-Ed2hnVNo5Azp7CvcH7DsvAeQ7Oo=", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3", - "side-channel-map": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/siginfo": { - "version": "2.0.0", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/siginfo/-/siginfo-2.0.0.tgz", - "integrity": "sha1-MudscLeXJOO7Vny51UPrhYzPrzA=", - "dev": true, - "license": "ISC" - }, - "node_modules/source-map": { - "version": "0.7.6", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/source-map/-/source-map-0.7.6.tgz", - "integrity": "sha1-o2WKuH5bZCnIofO6AIPUxhyj7wI=", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">= 12" - } - }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha1-HOVlD93YerwJnto33P8CTCZnrkY=", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/space-separated-tokens": { - "version": "2.0.2", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", - "integrity": "sha1-Hs2dI1CjhEVyw/SjErzrAYNIhZ8=", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/stackback": { - "version": "0.0.2", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/stackback/-/stackback-0.0.2.tgz", - "integrity": "sha1-Gsig2Ug4SNFpXkGLbQMaPDzmjjs=", - "dev": true, - "license": "MIT" - }, - "node_modules/statuses": { - "version": "2.0.2", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/statuses/-/statuses-2.0.2.tgz", - "integrity": "sha1-j3XuzvdlteHPzcCA2llAntQk44I=", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/std-env": { - "version": "4.2.0", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/std-env/-/std-env-4.2.0.tgz", - "integrity": "sha1-jr4OxgSFZoq0ciezEvQlTN+AydM=", - "dev": true, - "license": "MIT" - }, - "node_modules/string-width": { - "version": "7.2.0", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/string-width/-/string-width-7.2.0.tgz", - "integrity": "sha1-tbuOIWXOJ11NQ0dt0nAK2Qkdttw=", - "license": "MIT", - "dependencies": { - "emoji-regex": "^10.3.0", - "get-east-asian-width": "^1.0.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/stringify-entities": { - "version": "4.0.4", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/stringify-entities/-/stringify-entities-4.0.4.tgz", - "integrity": "sha1-s7ee9fJ3zErHPK6wI2xbqTmzpPM=", - "license": "MIT", - "dependencies": { - "character-entities-html4": "^2.0.0", - "character-entities-legacy": "^3.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/strip-ansi": { - "version": "7.2.0", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/strip-ansi/-/strip-ansi-7.2.0.tgz", - "integrity": "sha1-0iomlSKDamJ6+NBLXD/Sx/o+MuM=", - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.2.2" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha1-MfEoGzgyYwQ0gxwxDAHMzajL4AY=", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/style-mod": { - "version": "4.1.3", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/style-mod/-/style-mod-4.1.3.tgz", - "integrity": "sha1-bpASJVu3mb2sN+KI92cbXXG/n3M=", - "license": "MIT" - }, - "node_modules/style-to-js": { - "version": "1.1.21", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/style-to-js/-/style-to-js-1.1.21.tgz", - "integrity": "sha1-KQiUEYf4V+eeKOnNeACLmgs+Do0=", - "license": "MIT", - "dependencies": { - "style-to-object": "1.0.14" - } - }, - "node_modules/style-to-object": { - "version": "1.0.14", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/style-to-object/-/style-to-object-1.0.14.tgz", - "integrity": "sha1-HSLw5yZruMbYyuXK9OxPAF4I9hE=", - "license": "MIT", - "dependencies": { - "inline-style-parser": "0.2.7" - } - }, - "node_modules/sucrase": { - "version": "3.35.1", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/sucrase/-/sucrase-3.35.1.tgz", - "integrity": "sha1-RhnqUDk/6L0K5QccJqvZsuNGv+E=", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.2", - "commander": "^4.0.0", - "lines-and-columns": "^1.1.6", - "mz": "^2.7.0", - "pirates": "^4.0.1", - "tinyglobby": "^0.2.11", - "ts-interface-checker": "^0.1.9" - }, - "bin": { - "sucrase": "bin/sucrase", - "sucrase-node": "bin/sucrase-node" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/sucrase/node_modules/commander": { - "version": "4.1.1", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/commander/-/commander-4.1.1.tgz", - "integrity": "sha1-n9YCvZNilOnp70aj9NaWQESxgGg=", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha1-G33NyzK4E4gBs+R4umpRyqiWSNo=", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/thenify": { - "version": "3.3.1", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/thenify/-/thenify-3.3.1.tgz", - "integrity": "sha1-iTLmhqQGYDigFt2eLKRq3Zg4qV8=", - "dev": true, - "license": "MIT", - "dependencies": { - "any-promise": "^1.0.0" - } - }, - "node_modules/thenify-all": { - "version": "1.6.0", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/thenify-all/-/thenify-all-1.6.0.tgz", - "integrity": "sha1-GhkY1ALY/D+Y+/I02wvMjMEOlyY=", - "dev": true, - "license": "MIT", - "dependencies": { - "thenify": ">= 3.1.0 < 4" - }, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/tinybench": { - "version": "2.9.0", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/tinybench/-/tinybench-2.9.0.tgz", - "integrity": "sha1-EDyfi6bXI3pHq23R3P93JRhjQms=", - "dev": true, - "license": "MIT" - }, - "node_modules/tinyexec": { - "version": "0.3.2", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/tinyexec/-/tinyexec-0.3.2.tgz", - "integrity": "sha1-lBeU5leoXklld5lcbu9m9T9Cs9I=", - "dev": true, - "license": "MIT" - }, - "node_modules/tinyglobby": { - "version": "0.2.17", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/tinyglobby/-/tinyglobby-0.2.17.tgz", - "integrity": "sha1-ViqabJ6ys7Ej05cZ+a9btE/NdjE=", - "dev": true, - "license": "MIT", - "dependencies": { - "fdir": "^6.5.0", - "picomatch": "^4.0.4" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" - } - }, - "node_modules/tinyrainbow": { - "version": "3.1.0", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/tinyrainbow/-/tinyrainbow-3.1.0.tgz", - "integrity": "sha1-HYpiOJP5XPCi3bnl0RFQ4ZFAlCE=", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha1-FkjESq58jZiKMmAY7XL1tN0DkuQ=", - "dev": true, - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha1-O+NDIaiKgg7RvYDfqjPkefu43TU=", - "license": "MIT", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/topojson-client": { - "version": "3.1.0", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/topojson-client/-/topojson-client-3.1.0.tgz", - "integrity": "sha1-Iuix7QiiuSL+60r29Ttu8JpGe5k=", - "license": "ISC", - "dependencies": { - "commander": "2" - }, - "bin": { - "topo2geo": "bin/topo2geo", - "topomerge": "bin/topomerge", - "topoquantize": "bin/topoquantize" - } - }, - "node_modules/topojson-client/node_modules/commander": { - "version": "2.20.3", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/commander/-/commander-2.20.3.tgz", - "integrity": "sha1-/UhehMA+tIgcIHIrpIA16FMa6zM=", - "license": "MIT" - }, - "node_modules/tree-kill": { - "version": "1.2.2", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/tree-kill/-/tree-kill-1.2.2.tgz", - "integrity": "sha1-TKCakJLIi3OnzcXooBtQeweQoMw=", - "dev": true, - "license": "MIT", - "bin": { - "tree-kill": "cli.js" - } - }, - "node_modules/trim-lines": { - "version": "3.0.1", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/trim-lines/-/trim-lines-3.0.1.tgz", - "integrity": "sha1-2ALjMqB9+GHEiALAQyEBexvYczg=", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/trough": { - "version": "2.2.0", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/trough/-/trough-2.2.0.tgz", - "integrity": "sha1-lKYL1r03XBUsHfkRpLEdWwJW9Q8=", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/ts-api-utils": { - "version": "2.5.0", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/ts-api-utils/-/ts-api-utils-2.5.0.tgz", - "integrity": "sha1-Ss1KFV4ic0mQpe0f6el/ETvLN8E=", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18.12" - }, - "peerDependencies": { - "typescript": ">=4.8.4" - } - }, - "node_modules/ts-interface-checker": { - "version": "0.1.13", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", - "integrity": "sha1-eE/T1nlyK8EDsbS4AwvN212yppk=", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/tslib": { - "version": "2.3.0", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/tslib/-/tslib-2.3.0.tgz", - "integrity": "sha1-gDuM2rPhK6WBpMpByIObuw2ssJ4=", - "license": "0BSD" - }, - "node_modules/tsup": { - "version": "8.5.1", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/tsup/-/tsup-8.5.1.tgz", - "integrity": "sha1-qceodbkzRL33BgDe3XjnD4jsmmU=", - "dev": true, - "license": "MIT", - "dependencies": { - "bundle-require": "^5.1.0", - "cac": "^6.7.14", - "chokidar": "^4.0.3", - "consola": "^3.4.0", - "debug": "^4.4.0", - "esbuild": "^0.27.0", - "fix-dts-default-cjs-exports": "^1.0.0", - "joycon": "^3.1.1", - "picocolors": "^1.1.1", - "postcss-load-config": "^6.0.1", - "resolve-from": "^5.0.0", - "rollup": "^4.34.8", - "source-map": "^0.7.6", - "sucrase": "^3.35.0", - "tinyexec": "^0.3.2", - "tinyglobby": "^0.2.11", - "tree-kill": "^1.2.2" - }, - "bin": { - "tsup": "dist/cli-default.js", - "tsup-node": "dist/cli-node.js" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@microsoft/api-extractor": "^7.36.0", - "@swc/core": "^1", - "postcss": "^8.4.12", - "typescript": ">=4.5.0" - }, - "peerDependenciesMeta": { - "@microsoft/api-extractor": { - "optional": true - }, - "@swc/core": { - "optional": true - }, - "postcss": { - "optional": true - }, - "typescript": { - "optional": true - } - } - }, - "node_modules/type-check": { - "version": "0.4.0", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha1-B7ggO/pwVsBlcFDjzNLDdzC6uPE=", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/type-is": { - "version": "2.1.0", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/type-is/-/type-is-2.1.0.tgz", - "integrity": "sha1-cdGnBTKTWC4WrJ8+uvGrmqSeVXA=", - "license": "MIT", - "dependencies": { - "content-type": "^2.0.0", - "media-typer": "^1.1.0", - "mime-types": "^3.0.0" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/type-is/node_modules/content-type": { - "version": "2.0.0", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/content-type/-/content-type-2.0.0.tgz", - "integrity": "sha1-L7Pt5p3/oK94ynxM51iWgGOLVt8=", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha1-W09Z4VMQqxeiFvXWz1PuR27eZw8=", - "devOptional": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/typescript-eslint": { - "version": "8.64.0", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/typescript-eslint/-/typescript-eslint-8.64.0.tgz", - "integrity": "sha1-SYTa5N6dyL+JKs9cOU0KKl8Iw+E=", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/eslint-plugin": "8.64.0", - "@typescript-eslint/parser": "8.64.0", - "@typescript-eslint/typescript-estree": "8.64.0", - "@typescript-eslint/utils": "8.64.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/ufo": { - "version": "1.6.4", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/ufo/-/ufo-1.6.4.tgz", - "integrity": "sha1-eo+4dfzGOC0sfQs2knOLBQCpJGc=", - "dev": true, - "license": "MIT" - }, - "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha1-aR0ArzkJvpOn+qE75hs6W1DvEss=", - "dev": true, - "license": "MIT" - }, - "node_modules/unified": { - "version": "11.0.5", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/unified/-/unified-11.0.5.tgz", - "integrity": "sha1-9mZ3YQpcCp7pDKsrjU1mA3Am2eE=", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0", - "bail": "^2.0.0", - "devlop": "^1.0.0", - "extend": "^3.0.0", - "is-plain-obj": "^4.0.0", - "trough": "^2.0.0", - "vfile": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-find-after": { - "version": "5.0.0", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/unist-util-find-after/-/unist-util-find-after-5.0.0.tgz", - "integrity": "sha1-P8zBsIa1bzTIt5jh/5C1xURo6JY=", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0", - "unist-util-is": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-is": { - "version": "6.0.1", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/unist-util-is/-/unist-util-is-6.0.1.tgz", - "integrity": "sha1-0KP4by3Q23rNfYwkeAgLXGf5xqk=", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-position": { - "version": "5.0.0", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/unist-util-position/-/unist-util-position-5.0.0.tgz", - "integrity": "sha1-Z48gq1yhIHqX1+qKOINzyc+Ja+Q=", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-remove-position": { - "version": "5.0.0", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/unist-util-remove-position/-/unist-util-remove-position-5.0.0.tgz", - "integrity": "sha1-/qaKJWWECclGBAi8a0mRuWW1IWM=", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0", - "unist-util-visit": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-stringify-position": { - "version": "4.0.0", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", - "integrity": "sha1-RJxuIaiA4IVb9aq63rOnQDFKusI=", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-visit": { - "version": "5.1.0", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/unist-util-visit/-/unist-util-visit-5.1.0.tgz", - "integrity": "sha1-mioosKp2oV4NpwoIpYY6LwYOJGg=", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0", - "unist-util-is": "^6.0.0", - "unist-util-visit-parents": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-visit-parents": { - "version": "6.0.2", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", - "integrity": "sha1-d333+5hlLOFrS3zZmdChpA76OgI=", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0", - "unist-util-is": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha1-mxpSWVIlhZ5V9mnZKPiMbFfyp34=", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/use-sync-external-store": { - "version": "1.6.0", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", - "integrity": "sha1-sXS/plyytSZzLZ8qwKQIAnh28y0=", - "license": "MIT", - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, - "node_modules/vary": { - "version": "1.1.2", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/vary/-/vary-1.1.2.tgz", - "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/vega": { - "version": "6.2.0", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/vega/-/vega-6.2.0.tgz", - "integrity": "sha1-NMLeg7AOcB4EAClziybx7JkvMn8=", - "license": "BSD-3-Clause", - "dependencies": { - "vega-crossfilter": "~5.1.0", - "vega-dataflow": "~6.1.0", - "vega-encode": "~5.1.0", - "vega-event-selector": "~4.0.0", - "vega-expression": "~6.1.0", - "vega-force": "~5.1.0", - "vega-format": "~2.1.0", - "vega-functions": "~6.1.0", - "vega-geo": "~5.1.0", - "vega-hierarchy": "~5.1.0", - "vega-label": "~2.1.0", - "vega-loader": "~5.1.0", - "vega-parser": "~7.1.0", - "vega-projection": "~2.1.0", - "vega-regression": "~2.1.0", - "vega-runtime": "~7.1.0", - "vega-scale": "~8.1.0", - "vega-scenegraph": "~5.1.0", - "vega-statistics": "~2.0.0", - "vega-time": "~3.1.0", - "vega-transforms": "~5.1.0", - "vega-typings": "~2.1.0", - "vega-util": "~2.1.0", - "vega-view": "~6.1.0", - "vega-view-transforms": "~5.1.0", - "vega-voronoi": "~5.1.0", - "vega-wordcloud": "~5.1.0" - }, - "funding": { - "url": "https://app.hubspot.com/payments/GyPC972GD9Rt" - } - }, - "node_modules/vega-canvas": { - "version": "2.0.0", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/vega-canvas/-/vega-canvas-2.0.0.tgz", - "integrity": "sha1-Rwneto+bT9dHWVe+2Z8Ww428B7g=", - "license": "BSD-3-Clause" - }, - "node_modules/vega-crossfilter": { - "version": "5.1.0", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/vega-crossfilter/-/vega-crossfilter-5.1.0.tgz", - "integrity": "sha1-9MVtngwxcFyuQc0ONavN7iDAxIM=", - "license": "BSD-3-Clause", - "dependencies": { - "d3-array": "^3.2.4", - "vega-dataflow": "^6.1.0", - "vega-util": "^2.1.0" - } - }, - "node_modules/vega-dataflow": { - "version": "6.1.0", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/vega-dataflow/-/vega-dataflow-6.1.0.tgz", - "integrity": "sha1-H8SOpru+AC1FoaSO7meuoJelfFU=", - "license": "BSD-3-Clause", - "dependencies": { - "vega-format": "^2.1.0", - "vega-loader": "^5.1.0", - "vega-util": "^2.1.0" - } - }, - "node_modules/vega-embed": { - "version": "7.1.0", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/vega-embed/-/vega-embed-7.1.0.tgz", - "integrity": "sha1-QoQyuiz1uwlhwNNI956E7Kn8jPs=", - "license": "BSD-3-Clause", - "dependencies": { - "fast-json-patch": "^3.1.1", - "json-stringify-pretty-compact": "^4.0.0", - "semver": "^7.7.2", - "tslib": "^2.8.1", - "vega-interpreter": "^2.0.0", - "vega-schema-url-parser": "^3.0.2", - "vega-themes": "3.0.0", - "vega-tooltip": "1.0.0" - }, - "funding": { - "url": "https://app.hubspot.com/payments/GyPC972GD9Rt" - }, - "peerDependencies": { - "vega": "*", - "vega-lite": "*" - } - }, - "node_modules/vega-embed/node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha1-YS7+TtI11Wfoq6Xypfq3AoCt6D8=", - "license": "0BSD" - }, - "node_modules/vega-encode": { - "version": "5.1.0", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/vega-encode/-/vega-encode-5.1.0.tgz", - "integrity": "sha1-BfVriYgi4J35alyn8QF7n5ocTTs=", - "license": "BSD-3-Clause", - "dependencies": { - "d3-array": "^3.2.4", - "d3-interpolate": "^3.0.1", - "vega-dataflow": "^6.1.0", - "vega-scale": "^8.1.0", - "vega-util": "^2.1.0" - } - }, - "node_modules/vega-event-selector": { - "version": "4.0.0", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/vega-event-selector/-/vega-event-selector-4.0.0.tgz", - "integrity": "sha1-Ql6fJnHoWKGkW0tqf8RSygsiq78=", - "license": "BSD-3-Clause" - }, - "node_modules/vega-expression": { - "version": "6.1.0", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/vega-expression/-/vega-expression-6.1.0.tgz", - "integrity": "sha1-bONYo5ublTgGv/IA9vhPRBY8njg=", - "license": "BSD-3-Clause", - "dependencies": { - "@types/estree": "^1.0.8", - "vega-util": "^2.1.0" - } - }, - "node_modules/vega-force": { - "version": "5.1.0", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/vega-force/-/vega-force-5.1.0.tgz", - "integrity": "sha1-qnz47b4q47raBw80NWXfuEHlAak=", - "license": "BSD-3-Clause", - "dependencies": { - "d3-force": "^3.0.0", - "vega-dataflow": "^6.1.0", - "vega-util": "^2.1.0" - } - }, - "node_modules/vega-format": { - "version": "2.1.0", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/vega-format/-/vega-format-2.1.0.tgz", - "integrity": "sha1-RlLH7J+xt/+aLFDc1Jija6YUb9o=", - "license": "BSD-3-Clause", - "dependencies": { - "d3-array": "^3.2.4", - "d3-format": "^3.1.0", - "d3-time-format": "^4.1.0", - "vega-time": "^3.1.0", - "vega-util": "^2.1.0" - } - }, - "node_modules/vega-functions": { - "version": "6.1.1", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/vega-functions/-/vega-functions-6.1.1.tgz", - "integrity": "sha1-XU6XRqrd4rO3DY2j5jOIktBc2dI=", - "license": "BSD-3-Clause", - "dependencies": { - "d3-array": "^3.2.4", - "d3-color": "^3.1.0", - "d3-geo": "^3.1.1", - "vega-dataflow": "^6.1.0", - "vega-expression": "^6.1.0", - "vega-scale": "^8.1.0", - "vega-scenegraph": "^5.1.0", - "vega-selections": "^6.1.0", - "vega-statistics": "^2.0.0", - "vega-time": "^3.1.0", - "vega-util": "^2.1.0" - } - }, - "node_modules/vega-geo": { - "version": "5.1.0", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/vega-geo/-/vega-geo-5.1.0.tgz", - "integrity": "sha1-2P5q6RKtJ80rHCH1RadMB9oJNYk=", - "license": "BSD-3-Clause", - "dependencies": { - "d3-array": "^3.2.4", - "d3-color": "^3.1.0", - "d3-geo": "^3.1.1", - "vega-canvas": "^2.0.0", - "vega-dataflow": "^6.1.0", - "vega-projection": "^2.1.0", - "vega-statistics": "^2.0.0", - "vega-util": "^2.1.0" - } - }, - "node_modules/vega-hierarchy": { - "version": "5.1.0", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/vega-hierarchy/-/vega-hierarchy-5.1.0.tgz", - "integrity": "sha1-Qjdw3Ry0aENw8jpojcW22tE5nb8=", - "license": "BSD-3-Clause", - "dependencies": { - "d3-hierarchy": "^3.1.2", - "vega-dataflow": "^6.1.0", - "vega-util": "^2.1.0" - } - }, - "node_modules/vega-interpreter": { - "version": "2.2.1", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/vega-interpreter/-/vega-interpreter-2.2.1.tgz", - "integrity": "sha1-lH2PKIelnRJK3diE0BsC9lc/uFE=", - "license": "BSD-3-Clause", - "dependencies": { - "vega-util": "^2.1.0" - } - }, - "node_modules/vega-label": { - "version": "2.1.0", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/vega-label/-/vega-label-2.1.0.tgz", - "integrity": "sha1-vZd80U6bBi/OMVk6LbKBmqnvssk=", - "license": "BSD-3-Clause", - "dependencies": { - "vega-canvas": "^2.0.0", - "vega-dataflow": "^6.1.0", - "vega-scenegraph": "^5.1.0", - "vega-util": "^2.1.0" - } - }, - "node_modules/vega-lite": { - "version": "6.4.3", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/vega-lite/-/vega-lite-6.4.3.tgz", - "integrity": "sha1-mXPTr+E5H7CIB7wlheEp4tr3o2s=", - "license": "BSD-3-Clause", - "dependencies": { - "json-stringify-pretty-compact": "~4.0.0", - "tslib": "~2.8.1", - "vega-event-selector": "~4.0.0", - "vega-expression": "~6.1.0", - "vega-util": "~2.1.0", - "yargs": "~18.0.0" - }, - "bin": { - "vl2pdf": "bin/vl2pdf", - "vl2png": "bin/vl2png", - "vl2svg": "bin/vl2svg", - "vl2vg": "bin/vl2vg" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://app.hubspot.com/payments/GyPC972GD9Rt" - }, - "peerDependencies": { - "vega": "^6.0.0" - } - }, - "node_modules/vega-lite/node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha1-YS7+TtI11Wfoq6Xypfq3AoCt6D8=", - "license": "0BSD" - }, - "node_modules/vega-loader": { - "version": "5.1.0", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/vega-loader/-/vega-loader-5.1.0.tgz", - "integrity": "sha1-aTePxNRujUVzrTCPdkZOZrAleeY=", - "license": "BSD-3-Clause", - "dependencies": { - "d3-dsv": "^3.0.1", - "topojson-client": "^3.1.0", - "vega-format": "^2.1.0", - "vega-util": "^2.1.0" - } - }, - "node_modules/vega-parser": { - "version": "7.1.0", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/vega-parser/-/vega-parser-7.1.0.tgz", - "integrity": "sha1-IO4OcKbs24yzTvFt7tSErWjECFA=", - "license": "BSD-3-Clause", - "dependencies": { - "vega-dataflow": "^6.1.0", - "vega-event-selector": "^4.0.0", - "vega-functions": "^6.1.0", - "vega-scale": "^8.1.0", - "vega-util": "^2.1.0" - } - }, - "node_modules/vega-projection": { - "version": "2.1.0", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/vega-projection/-/vega-projection-2.1.0.tgz", - "integrity": "sha1-zkYpHveKdBjHVnkQMpbWL0mvrBQ=", - "license": "BSD-3-Clause", - "dependencies": { - "d3-geo": "^3.1.1", - "d3-geo-projection": "^4.0.0", - "vega-scale": "^8.1.0" - } - }, - "node_modules/vega-regression": { - "version": "2.1.0", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/vega-regression/-/vega-regression-2.1.0.tgz", - "integrity": "sha1-0/0QPpegruVa4qeO2BWI+13LngM=", - "license": "BSD-3-Clause", - "dependencies": { - "d3-array": "^3.2.4", - "vega-dataflow": "^6.1.0", - "vega-statistics": "^2.0.0", - "vega-util": "^2.1.0" - } - }, - "node_modules/vega-runtime": { - "version": "7.1.0", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/vega-runtime/-/vega-runtime-7.1.0.tgz", - "integrity": "sha1-GVnWFoY4+Fvc5NFXEXrKatH2n6w=", - "license": "BSD-3-Clause", - "dependencies": { - "vega-dataflow": "^6.1.0", - "vega-util": "^2.1.0" - } - }, - "node_modules/vega-scale": { - "version": "8.1.0", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/vega-scale/-/vega-scale-8.1.0.tgz", - "integrity": "sha1-oGs6qNYK5GrY89ierg506z0SAOM=", - "license": "BSD-3-Clause", - "dependencies": { - "d3-array": "^3.2.4", - "d3-interpolate": "^3.0.1", - "d3-scale": "^4.0.2", - "d3-scale-chromatic": "^3.1.0", - "vega-time": "^3.1.0", - "vega-util": "^2.1.0" - } - }, - "node_modules/vega-scenegraph": { - "version": "5.1.0", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/vega-scenegraph/-/vega-scenegraph-5.1.0.tgz", - "integrity": "sha1-OzwNhxeZ/oS8VjJW17nVS8LhM2g=", - "license": "BSD-3-Clause", - "dependencies": { - "d3-path": "^3.1.0", - "d3-shape": "^3.2.0", - "vega-canvas": "^2.0.0", - "vega-loader": "^5.1.0", - "vega-scale": "^8.1.0", - "vega-util": "^2.1.0" - } - }, - "node_modules/vega-schema-url-parser": { - "version": "3.0.2", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/vega-schema-url-parser/-/vega-schema-url-parser-3.0.2.tgz", - "integrity": "sha1-lfCmrX/JNBr8BUlcGgdMYvpxxFY=", - "license": "BSD-3-Clause" - }, - "node_modules/vega-selections": { - "version": "6.1.2", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/vega-selections/-/vega-selections-6.1.2.tgz", - "integrity": "sha1-Nkbbap/B1yWWm4tYQeXTM8Hw+AM=", - "license": "BSD-3-Clause", - "dependencies": { - "d3-array": "3.2.4", - "vega-expression": "^6.1.0", - "vega-util": "^2.1.0" - } - }, - "node_modules/vega-statistics": { - "version": "2.0.0", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/vega-statistics/-/vega-statistics-2.0.0.tgz", - "integrity": "sha1-nJY2wgaCrpjoiH+Pqw6CwkZqc2o=", - "license": "BSD-3-Clause", - "dependencies": { - "d3-array": "^3.2.4" - } - }, - "node_modules/vega-themes": { - "version": "3.0.0", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/vega-themes/-/vega-themes-3.0.0.tgz", - "integrity": "sha1-lvP3nDkvwwIOXRLvWP5k9RbTbh4=", - "license": "BSD-3-Clause", - "funding": { - "url": "https://app.hubspot.com/payments/GyPC972GD9Rt" - }, - "peerDependencies": { - "vega": "*", - "vega-lite": "*" - } - }, - "node_modules/vega-time": { - "version": "3.1.0", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/vega-time/-/vega-time-3.1.0.tgz", - "integrity": "sha1-TiDF1g4/foJ6M9spvUhV9AoK48s=", - "license": "BSD-3-Clause", - "dependencies": { - "d3-array": "^3.2.4", - "d3-time": "^3.1.0", - "vega-util": "^2.1.0" - } - }, - "node_modules/vega-tooltip": { - "version": "1.0.0", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/vega-tooltip/-/vega-tooltip-1.0.0.tgz", - "integrity": "sha1-xoKcm831voU4UDB2ZcnyEyEIP/I=", - "license": "BSD-3-Clause", - "dependencies": { - "vega-util": "^2.0.0" - }, - "funding": { - "url": "https://app.hubspot.com/payments/GyPC972GD9Rt" - } - }, - "node_modules/vega-transforms": { - "version": "5.1.0", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/vega-transforms/-/vega-transforms-5.1.0.tgz", - "integrity": "sha1-TpXNfEdzqlYJKNEDhaDTPqJ0jKo=", - "license": "BSD-3-Clause", - "dependencies": { - "d3-array": "^3.2.4", - "vega-dataflow": "^6.1.0", - "vega-statistics": "^2.0.0", - "vega-time": "^3.1.0", - "vega-util": "^2.1.0" - } - }, - "node_modules/vega-typings": { - "version": "2.1.0", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/vega-typings/-/vega-typings-2.1.0.tgz", - "integrity": "sha1-HB/lSMDwCZeCAkat4NPYE7h7/XY=", - "license": "BSD-3-Clause", - "dependencies": { - "@types/geojson": "7946.0.16", - "vega-event-selector": "^4.0.0", - "vega-expression": "^6.1.0", - "vega-util": "^2.1.0" - } - }, - "node_modules/vega-util": { - "version": "2.1.1", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/vega-util/-/vega-util-2.1.1.tgz", - "integrity": "sha1-0fSU4C/dK2oWeadAuFJUuGTN1Zc=", - "license": "BSD-3-Clause" - }, - "node_modules/vega-view": { - "version": "6.1.0", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/vega-view/-/vega-view-6.1.0.tgz", - "integrity": "sha1-VZb3jF68qNy1f+ykD9McuCZf0E4=", - "license": "BSD-3-Clause", - "dependencies": { - "d3-array": "^3.2.4", - "d3-timer": "^3.0.1", - "vega-dataflow": "^6.1.0", - "vega-format": "^2.1.0", - "vega-functions": "^6.1.0", - "vega-runtime": "^7.1.0", - "vega-scenegraph": "^5.1.0", - "vega-util": "^2.1.0" - } - }, - "node_modules/vega-view-transforms": { - "version": "5.1.0", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/vega-view-transforms/-/vega-view-transforms-5.1.0.tgz", - "integrity": "sha1-HzH3Xvz5mziWnnUAQ625Ivzsbz4=", - "license": "BSD-3-Clause", - "dependencies": { - "vega-dataflow": "^6.1.0", - "vega-scenegraph": "^5.1.0", - "vega-util": "^2.1.0" - } - }, - "node_modules/vega-voronoi": { - "version": "5.1.0", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/vega-voronoi/-/vega-voronoi-5.1.0.tgz", - "integrity": "sha1-kpVrnXjwbjkYlw/ITQaXTiS59S8=", - "license": "BSD-3-Clause", - "dependencies": { - "d3-delaunay": "^6.0.4", - "vega-dataflow": "^6.1.0", - "vega-util": "^2.1.0" - } - }, - "node_modules/vega-wordcloud": { - "version": "5.1.0", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/vega-wordcloud/-/vega-wordcloud-5.1.0.tgz", - "integrity": "sha1-eqjcv2yDsZP+cftkEL4VrSxyheY=", - "license": "BSD-3-Clause", - "dependencies": { - "vega-canvas": "^2.0.0", - "vega-dataflow": "^6.1.0", - "vega-scale": "^8.1.0", - "vega-statistics": "^2.0.0", - "vega-util": "^2.1.0" - } - }, - "node_modules/vfile": { - "version": "6.0.3", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/vfile/-/vfile-6.0.3.tgz", - "integrity": "sha1-NlKrHEllMYUr9VprrFevmB68OKs=", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0", - "vfile-message": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/vfile-location": { - "version": "5.0.3", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/vfile-location/-/vfile-location-5.0.3.tgz", - "integrity": "sha1-y56s0g8rZCbRlFHg6vo9CoRiJcM=", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0", - "vfile": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/vfile-message": { - "version": "4.0.3", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/vfile-message/-/vfile-message-4.0.3.tgz", - "integrity": "sha1-h7RN3de3DwZBwuPtCGS6c+LqjfQ=", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0", - "unist-util-stringify-position": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/vite": { - "version": "8.1.5", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/vite/-/vite-8.1.5.tgz", - "integrity": "sha1-zP/OPuSHsYhiI7JOuye5FnSCfTA=", - "dev": true, - "license": "MIT", - "dependencies": { - "lightningcss": "^1.32.0", - "picomatch": "^4.0.5", - "postcss": "^8.5.17", - "rolldown": "~1.1.5", - "tinyglobby": "^0.2.17" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^20.19.0 || >=22.12.0", - "@vitejs/devtools": "^0.3.0", - "esbuild": "^0.27.0 || ^0.28.0", - "jiti": ">=1.21.0", - "less": "^4.0.0", - "sass": "^1.70.0", - "sass-embedded": "^1.70.0", - "stylus": ">=0.54.8", - "sugarss": "^5.0.0", - "terser": "^5.16.0", - "tsx": "^4.8.1", - "yaml": "^2.4.2" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "@vitejs/devtools": { - "optional": true - }, - "esbuild": { - "optional": true - }, - "jiti": { - "optional": true - }, - "less": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true - } + "os": [ + "win32" + ], + "engines": { + "node": ">=12" } }, - "node_modules/vite-plugin-singlefile": { - "version": "2.3.3", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/vite-plugin-singlefile/-/vite-plugin-singlefile-2.3.3.tgz", - "integrity": "sha1-6FmupMDEt0/LumuvUn6I8q0J3mk=", + "node_modules/vite/node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", "dev": true, + "hasInstallScript": true, "license": "MIT", - "dependencies": { - "micromatch": "^4.0.8" + "bin": { + "esbuild": "bin/esbuild" }, "engines": { - "node": ">18.0.0" - }, - "peerDependencies": { - "rollup": "^4.59.0", - "vite": "^5.4.21 || ^6.0.0 || ^7.0.0 || ^8.0.0" + "node": ">=12" }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" } }, "node_modules/vitest": { - "version": "4.1.10", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/vitest/-/vitest-4.1.10.tgz", - "integrity": "sha1-fpKF7+JksRZwULejp/80eI4bevw=", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/expect": "4.1.10", - "@vitest/mocker": "4.1.10", - "@vitest/pretty-format": "4.1.10", - "@vitest/runner": "4.1.10", - "@vitest/snapshot": "4.1.10", - "@vitest/spy": "4.1.10", - "@vitest/utils": "4.1.10", - "es-module-lexer": "^2.0.0", - "expect-type": "^1.3.0", - "magic-string": "^0.30.21", - "obug": "^2.1.1", - "pathe": "^2.0.3", - "picomatch": "^4.0.3", - "std-env": "^4.0.0-rc.1", + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.9.tgz", + "integrity": "sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "2.1.9", + "@vitest/mocker": "2.1.9", + "@vitest/pretty-format": "^2.1.9", + "@vitest/runner": "2.1.9", + "@vitest/snapshot": "2.1.9", + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "debug": "^4.3.7", + "expect-type": "^1.1.0", + "magic-string": "^0.30.12", + "pathe": "^1.1.2", + "std-env": "^3.8.0", "tinybench": "^2.9.0", - "tinyexec": "^1.0.2", - "tinyglobby": "^0.2.15", - "tinyrainbow": "^3.1.0", - "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "tinyexec": "^0.3.1", + "tinypool": "^1.0.1", + "tinyrainbow": "^1.2.0", + "vite": "^5.0.0", + "vite-node": "2.1.9", "why-is-node-running": "^2.3.0" }, "bin": { "vitest": "vitest.mjs" }, "engines": { - "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + "node": "^18.0.0 || >=20.0.0" }, "funding": { "url": "https://opencollective.com/vitest" }, "peerDependencies": { "@edge-runtime/vm": "*", - "@opentelemetry/api": "^1.9.0", - "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", - "@vitest/browser-playwright": "4.1.10", - "@vitest/browser-preview": "4.1.10", - "@vitest/browser-webdriverio": "4.1.10", - "@vitest/coverage-istanbul": "4.1.10", - "@vitest/coverage-v8": "4.1.10", - "@vitest/ui": "4.1.10", + "@types/node": "^18.0.0 || >=20.0.0", + "@vitest/browser": "2.1.9", + "@vitest/ui": "2.1.9", "happy-dom": "*", - "jsdom": "*", - "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + "jsdom": "*" }, "peerDependenciesMeta": { "@edge-runtime/vm": { "optional": true }, - "@opentelemetry/api": { - "optional": true - }, "@types/node": { "optional": true }, - "@vitest/browser-playwright": { - "optional": true - }, - "@vitest/browser-preview": { - "optional": true - }, - "@vitest/browser-webdriverio": { - "optional": true - }, - "@vitest/coverage-istanbul": { - "optional": true - }, - "@vitest/coverage-v8": { + "@vitest/browser": { "optional": true }, "@vitest/ui": { @@ -9441,51 +3966,21 @@ }, "jsdom": { "optional": true - }, - "vite": { - "optional": false } } }, - "node_modules/vitest/node_modules/tinyexec": { - "version": "1.2.4", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/tinyexec/-/tinyexec-1.2.4.tgz", - "integrity": "sha1-rkW7Lt69qUxw9OqJfg8SQ+Rw23E=", + "node_modules/vitest/node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/void-elements": { - "version": "3.1.0", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/void-elements/-/void-elements-3.1.0.tgz", - "integrity": "sha1-YU9/v42AHwu18GYfWy9XhXUOTwk=", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/w3c-keyname": { - "version": "2.2.8", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/w3c-keyname/-/w3c-keyname-2.2.8.tgz", - "integrity": "sha1-exfIxog9TouGrIq6edOeiA+IacU=", "license": "MIT" }, - "node_modules/web-namespaces": { - "version": "2.0.1", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/web-namespaces/-/web-namespaces-2.0.1.tgz", - "integrity": "sha1-EBD/fGUOzLJZLOvur5obJT/UBpI=", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, "node_modules/which": { "version": "2.0.2", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/which/-/which-2.0.2.tgz", - "integrity": "sha1-fGqN0KY2oDJ+ELWckobu6T8/UbE=", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, "license": "ISC", "dependencies": { "isexe": "^2.0.0" @@ -9499,8 +3994,8 @@ }, "node_modules/why-is-node-running": { "version": "2.3.0", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/why-is-node-running/-/why-is-node-running-2.3.0.tgz", - "integrity": "sha1-o/aalxB/SUs83Dvd3Yg6fWXOvwQ=", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", "dev": true, "license": "MIT", "dependencies": { @@ -9516,76 +4011,18 @@ }, "node_modules/word-wrap": { "version": "1.2.5", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/word-wrap/-/word-wrap-1.2.5.tgz", - "integrity": "sha1-0sRcbdT7zmIaZvE2y+Mor9BBCzQ=", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" } }, - "node_modules/wrap-ansi": { - "version": "9.0.2", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/wrap-ansi/-/wrap-ansi-9.0.2.tgz", - "integrity": "sha1-lWgy3qlJQwbm0gnrhxZDu4c9fJg=", - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.2.1", - "string-width": "^7.0.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "license": "ISC" - }, - "node_modules/y18n": { - "version": "5.0.8", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha1-f0k00PfKjFb5UxSTndzS3ZHOHVU=", - "license": "ISC", - "engines": { - "node": ">=10" - } - }, - "node_modules/yargs": { - "version": "18.0.0", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/yargs/-/yargs-18.0.0.tgz", - "integrity": "sha1-bIQlmAYnOnRrCfV5CHtoo8LSW9E=", - "license": "MIT", - "dependencies": { - "cliui": "^9.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "string-width": "^7.2.0", - "y18n": "^5.0.5", - "yargs-parser": "^22.0.0" - }, - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=23" - } - }, - "node_modules/yargs-parser": { - "version": "22.0.0", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/yargs-parser/-/yargs-parser-22.0.0.tgz", - "integrity": "sha1-h7gglAUbBWdxc0bs0A/RSASzV8g=", - "license": "ISC", - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=23" - } - }, "node_modules/yocto-queue": { "version": "0.1.0", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha1-ApTrPe4FAo0x7hpfosVWpqrxChs=", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", "dev": true, "license": "MIT", "engines": { @@ -9594,307 +4031,6 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } - }, - "node_modules/zod": { - "version": "3.25.76", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/zod/-/zod-3.25.76.tgz", - "integrity": "sha1-JoQcP2/SKmonYOfMtxkXl2hHHjQ=", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - }, - "node_modules/zod-to-json-schema": { - "version": "3.25.2", - "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", - "integrity": "sha1-P6eZp7rdVUVBRy+2WEP9xGCy5ao=", - "license": "ISC", - "peerDependencies": { - "zod": "^3.25.28 || ^4" - } - }, - "node_modules/zrender": { - "version": "6.1.0", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/zrender/-/zrender-6.1.0.tgz", - "integrity": "sha1-w+8G5kJtXCiGW3GDA1aA1oXgATY=", - "license": "BSD-3-Clause", - "dependencies": { - "tslib": "2.3.0" - } - }, - "node_modules/zwitch": { - "version": "2.0.4", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/zwitch/-/zwitch-2.0.4.tgz", - "integrity": "sha1-yCfUsKy3b8PmhaTG7CkC1RBw6dc=", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "packages/flint-js": { - "name": "flint-chart", - "version": "0.4.1", - "license": "MIT", - "devDependencies": { - "@types/node": "^20.14.10", - "@typescript-eslint/eslint-plugin": "^8.16.0", - "@typescript-eslint/parser": "^8.16.0", - "eslint": "^9.15.0", - "eslint-plugin-unused-imports": "^4.4.1", - "prettier": "^3.3.0", - "rimraf": "^6.0.1", - "tsup": "^8.3.0", - "typescript": "^5.6.0", - "typescript-eslint": "^8.16.0", - "vitest": "^4.1.8" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "chart.js": "^4.0.0", - "echarts": "^5.0.0 || ^6.0.0", - "plotly.js": "^2.0.0 || ^3.0.0", - "vega": "^5.0.0 || ^6.0.0", - "vega-lite": "^5.0.0 || ^6.0.0" - }, - "peerDependenciesMeta": { - "chart.js": { - "optional": true - }, - "echarts": { - "optional": true - }, - "plotly.js": { - "optional": true - }, - "vega": { - "optional": true - }, - "vega-lite": { - "optional": true - } - } - }, - "packages/flint-mcp": { - "name": "flint-chart-mcp", - "version": "0.4.1", - "license": "MIT", - "dependencies": { - "@modelcontextprotocol/ext-apps": "^1.7.4", - "@modelcontextprotocol/sdk": "^1.29.0", - "@napi-rs/canvas": "^1.0.0", - "@resvg/resvg-js": "^2.6.2", - "chart.js": "^4.4.0", - "echarts": "^6.0.0", - "flint-chart": "^0.4.1", - "vega": "^6.0.0", - "vega-interpreter": "^2.2.1", - "vega-lite": "^6.0.0", - "zod": "^3.25.1" - }, - "bin": { - "flint-chart-mcp": "dist/cli.js" - }, - "devDependencies": { - "@types/node": "^20.14.10", - "@types/react": "^19.2.17", - "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^6.0.3", - "react": "^19.2.7", - "react-dom": "^19.2.7", - "rimraf": "^6.0.1", - "tsup": "^8.3.0", - "typescript": "^5.6.0", - "vite": "^8.1.0", - "vite-plugin-singlefile": "^2.3.3", - "vitest": "^4.1.8" - }, - "engines": { - "node": ">=18" - } - }, - "packages/flint-mcp/node_modules/@types/react": { - "version": "19.2.17", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@types/react/-/react-19.2.17.tgz", - "integrity": "sha1-3MrDZbqg8XNOwnD/S1HIlGXo3H8=", - "dev": true, - "license": "MIT", - "dependencies": { - "csstype": "^3.2.2" - } - }, - "packages/flint-mcp/node_modules/@types/react-dom": { - "version": "19.2.3", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@types/react-dom/-/react-dom-19.2.3.tgz", - "integrity": "sha1-weMF0VpSo+UI1U3Kdw0gLLY6vyw=", - "dev": true, - "license": "MIT", - "peerDependencies": { - "@types/react": "^19.2.0" - } - }, - "packages/flint-mcp/node_modules/react": { - "version": "19.2.7", - "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/react/-/react-19.2.7.tgz", - "integrity": "sha1-H0ehv8BvjsiFdSxvSvFDaan4Jgs=", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "packages/flint-mcp/node_modules/react-dom": { - "version": "19.2.7", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/react-dom/-/react-dom-19.2.7.tgz", - "integrity": "sha1-BFDcmundv/du8ZZAHNi4x/tGbMw=", - "dev": true, - "license": "MIT", - "dependencies": { - "scheduler": "^0.27.0" - }, - "peerDependencies": { - "react": "^19.2.7" - } - }, - "packages/flint-mcp/node_modules/scheduler": { - "version": "0.27.0", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/scheduler/-/scheduler-0.27.0.tgz", - "integrity": "sha1-DE74LWfR5cHjWej8dtOofwRf5b0=", - "dev": true, - "license": "MIT" - }, - "site": { - "name": "flint-chart-site", - "version": "0.0.0", - "dependencies": { - "@codemirror/lang-json": "^6.0.1", - "@fontsource-variable/inter": "^5.2.8", - "@uiw/react-codemirror": "^4.23.0", - "chart.js": "^4.5.1", - "echarts": "^6.0.0", - "flint-chart": "*", - "i18next": "^26.3.6", - "katex": "^0.17.0", - "plotly.js-dist-min": "^2.35.2", - "react": "^18.3.1", - "react-dom": "^18.3.1", - "react-i18next": "^17.0.10", - "react-markdown": "^10.1.0", - "react-router-dom": "^7.18.1", - "react-syntax-highlighter": "^16.1.1", - "rehype-katex": "^7.0.1", - "remark-gfm": "^4.0.1", - "remark-math": "^6.0.0", - "vega": "^6.0.0", - "vega-embed": "^7.1.0", - "vega-lite": "^6.4.1" - }, - "devDependencies": { - "@types/react": "^18.3.0", - "@types/react-dom": "^18.3.0", - "@types/react-syntax-highlighter": "^15.5.13", - "@vitejs/plugin-react-swc": "^3.7.0", - "typescript": "^5.6.0", - "vite": "^7.3.5" - } - }, - "site/node_modules/@rolldown/pluginutils": { - "version": "1.0.0-beta.27", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", - "integrity": "sha1-R9K/TO9tRwsi9YMbQg+JZOC/dV8=", - "dev": true, - "license": "MIT" - }, - "site/node_modules/@vitejs/plugin-react-swc": { - "version": "3.11.0", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@vitejs/plugin-react-swc/-/plugin-react-swc-3.11.0.tgz", - "integrity": "sha1-2CzDB9UwGXp3tQI4hgzzGYkP/Bc=", - "dev": true, - "license": "MIT", - "dependencies": { - "@rolldown/pluginutils": "1.0.0-beta.27", - "@swc/core": "^1.12.11" - }, - "peerDependencies": { - "vite": "^4 || ^5 || ^6 || ^7" - } - }, - "site/node_modules/vite": { - "version": "7.3.5", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/vite/-/vite-7.3.5.tgz", - "integrity": "sha1-kMLQt7lKIk5+fc8i0pEv8LUpEWU=", - "dev": true, - "license": "MIT", - "dependencies": { - "esbuild": "^0.27.0", - "fdir": "^6.5.0", - "picomatch": "^4.0.3", - "postcss": "^8.5.6", - "rollup": "^4.43.0", - "tinyglobby": "^0.2.15" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^20.19.0 || >=22.12.0", - "jiti": ">=1.21.0", - "less": "^4.0.0", - "lightningcss": "^1.21.0", - "sass": "^1.70.0", - "sass-embedded": "^1.70.0", - "stylus": ">=0.54.8", - "sugarss": "^5.0.0", - "terser": "^5.16.0", - "tsx": "^4.8.1", - "yaml": "^2.4.2" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "jiti": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true - } - } } } } diff --git a/site/src/routes/DocSectionPage.tsx b/site/src/routes/DocSectionPage.tsx index 14a1f9cb..0ede440a 100644 --- a/site/src/routes/DocSectionPage.tsx +++ b/site/src/routes/DocSectionPage.tsx @@ -48,6 +48,16 @@ export function DocSectionPage({ section }: { section: DocSection }) { } }, [slug, section, firstSlug, navigate, lp]); + // Reset scroll to top when the active doc changes, unless a heading anchor + // is pending (handled by the next effect). + useEffect(() => { + const stored = sessionStorage.getItem(DOC_SCROLL_TO_KEY); + const hash = location.hash ? decodeURIComponent(location.hash.slice(1)) : ''; + if (!stored && !hash) { + mainRef.current?.scrollTo({ top: 0 }); + } + }, [activeSlug, location.hash]); + useEffect(() => { const stored = sessionStorage.getItem(DOC_SCROLL_TO_KEY); const hash = location.hash ? decodeURIComponent(location.hash.slice(1)) : ''; From 77b08f007e3019c415dbe054893e0c65188e6813 Mon Sep 17 00:00:00 2001 From: Alper Sarikaya Date: Thu, 30 Jul 2026 16:29:03 -0700 Subject: [PATCH 036/164] revert package-lock.json changes --- package-lock.json | 10682 ++++++++++++++++++++++++++++++++++---------- 1 file changed, 8273 insertions(+), 2409 deletions(-) diff --git a/package-lock.json b/package-lock.json index 231e2fe7..0553d4e3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,58 +1,204 @@ { - "name": "flint-chart", - "version": "0.1.0", + "name": "flint-chart-monorepo", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "flint-chart", - "version": "0.1.0", - "license": "MIT", + "name": "flint-chart-monorepo", + "hasInstallScript": true, + "workspaces": [ + "packages/flint-js", + "packages/flint-mcp", + "site" + ], "devDependencies": { - "@types/node": "^20.14.10", - "@typescript-eslint/eslint-plugin": "^8.16.0", - "@typescript-eslint/parser": "^8.16.0", - "eslint": "^9.15.0", - "eslint-plugin-unused-imports": "^4.4.1", - "prettier": "^3.3.0", - "rimraf": "^6.0.1", - "tsup": "^8.3.0", - "typescript": "^5.6.0", - "typescript-eslint": "^8.16.0", - "vitest": "^2.1.0" + "@types/react": "^18.3.0", + "@types/react-dom": "^18.3.0", + "react": "^18.3.1", + "react-dom": "^18.3.1" }, "engines": { "node": ">=18" }, - "peerDependencies": { - "chart.js": "^4.0.0", - "echarts": "^5.0.0 || ^6.0.0", - "gofish-graphics": "^0.0.22", - "vega": "^5.0.0 || ^6.0.0", - "vega-lite": "^5.0.0 || ^6.0.0" - }, - "peerDependenciesMeta": { - "chart.js": { - "optional": true - }, - "echarts": { - "optional": true - }, - "gofish-graphics": { - "optional": true - }, - "vega": { - "optional": true - }, - "vega-lite": { - "optional": true - } + "optionalDependencies": { + "@rollup/rollup-linux-x64-gnu": "^4.0.0", + "@swc/core-linux-x64-gnu": "1.15.33" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.7", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha1-EgIkUMRaTabY2Ch7GKT/Ldsj92g=", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@codemirror/autocomplete": { + "version": "6.20.3", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@codemirror/autocomplete/-/autocomplete-6.20.3.tgz", + "integrity": "sha1-aWt0AxLGqWLhRWe0mjZhtZJLxa4=", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.17.0", + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@codemirror/commands": { + "version": "6.10.4", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@codemirror/commands/-/commands-6.10.4.tgz", + "integrity": "sha1-ZN7BvQQ5dus0TjzJ0VrxO29yS/0=", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.7.0", + "@codemirror/view": "^6.27.0", + "@lezer/common": "^1.1.0" + } + }, + "node_modules/@codemirror/lang-json": { + "version": "6.0.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@codemirror/lang-json/-/lang-json-6.0.2.tgz", + "integrity": "sha1-BUsWBnEwZmfiXYA4UoYEmEGDYXk=", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@lezer/json": "^1.0.0" + } + }, + "node_modules/@codemirror/language": { + "version": "6.12.4", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@codemirror/language/-/language-6.12.4.tgz", + "integrity": "sha1-AecP1ao6igZ/8d/sddW2OUzfoFg=", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.23.0", + "@lezer/common": "^1.5.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0", + "style-mod": "^4.0.0" + } + }, + "node_modules/@codemirror/lint": { + "version": "6.9.7", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@codemirror/lint/-/lint-6.9.7.tgz", + "integrity": "sha1-hB/HM2dDidkf5JocNAJ607q98QU=", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.42.0", + "crelt": "^1.0.5" + } + }, + "node_modules/@codemirror/search": { + "version": "6.7.1", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@codemirror/search/-/search-6.7.1.tgz", + "integrity": "sha1-JSOnYocdGK2YLtwtSy+h2kg7A5I=", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.37.0", + "crelt": "^1.0.5" + } + }, + "node_modules/@codemirror/state": { + "version": "6.7.1", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@codemirror/state/-/state-6.7.1.tgz", + "integrity": "sha1-noihdEjB28e1Csvu7Jee18zx1vw=", + "license": "MIT", + "dependencies": { + "@marijn/find-cluster-break": "^1.0.0" + } + }, + "node_modules/@codemirror/theme-one-dark": { + "version": "6.1.3", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@codemirror/theme-one-dark/-/theme-one-dark-6.1.3.tgz", + "integrity": "sha1-Hbtz9uc8U8Eq0q7Z9IwmPE5j6jc=", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/highlight": "^1.0.0" + } + }, + "node_modules/@codemirror/view": { + "version": "6.43.6", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@codemirror/view/-/view-6.43.6.tgz", + "integrity": "sha1-geTusoVefLbGIwX7qN8xm1R2KAw=", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.7.0", + "crelt": "^1.0.6", + "style-mod": "^4.1.0", + "w3c-keyname": "^2.2.4" + } + }, + "node_modules/@emnapi/core": { + "version": "1.11.1", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@emnapi/core/-/core-1.11.1.tgz", + "integrity": "sha1-ueEGTzprFjHiQeY460jXNr/TcqY=", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/core/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha1-YS7+TtI11Wfoq6Xypfq3AoCt6D8=", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@emnapi/runtime/-/runtime-1.11.1.tgz", + "integrity": "sha1-WPHz1dgamxL3k6tojJY3GQECfCQ=", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha1-YS7+TtI11Wfoq6Xypfq3AoCt6D8=", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", + "integrity": "sha1-TJO+z1v6OxPRu9zAau44MhrYE5o=", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" } }, + "node_modules/@emnapi/wasi-threads/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha1-YS7+TtI11Wfoq6Xypfq3AoCt6D8=", + "dev": true, + "license": "0BSD", + "optional": true + }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", - "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "version": "0.28.1", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha1-egGo0uwvuy2seK2tCbD6eB5Agr4=", "cpu": [ "ppc64" ], @@ -67,9 +213,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", - "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "version": "0.28.1", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha1-cEvSl95tdi3lTqu+r79V9nVqvi8=", "cpu": [ "arm" ], @@ -84,9 +230,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", - "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "version": "0.28.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha1-tUCifRTkr9BYSWpNvsTT9BTbEQo=", "cpu": [ "arm64" ], @@ -101,9 +247,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", - "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "version": "0.28.1", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha1-0csWbTSw+/D+irRgpVlPJKN4cB4=", "cpu": [ "x64" ], @@ -118,9 +264,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", - "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "version": "0.28.1", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha1-EDSyZFf8iGNo/mG70J9lP2r6jlQ=", "cpu": [ "arm64" ], @@ -135,9 +281,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", - "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "version": "0.28.1", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha1-ZVVqQyoeTXIDLYIYwZMvzKGkl3I=", "cpu": [ "x64" ], @@ -152,9 +298,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", - "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "version": "0.28.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha1-LmHgWS+QMNfj2uGO4l68U1kYrvY=", "cpu": [ "arm64" ], @@ -169,9 +315,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", - "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "version": "0.28.1", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha1-yV7CiZWe+AecTcqBeh4sS+Zrm9M=", "cpu": [ "x64" ], @@ -186,9 +332,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", - "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "version": "0.28.1", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha1-wJoPZ5F1kqwN6JKpvk04FN69Kmw=", "cpu": [ "arm" ], @@ -203,9 +349,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", - "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "version": "0.28.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha1-QLIhdd2gYYLz7oFBGGxf8wTEpxc=", "cpu": [ "arm64" ], @@ -220,9 +366,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", - "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "version": "0.28.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha1-pYD5xnZ5eDOJHlGfx6EzfIr9jbM=", "cpu": [ "ia32" ], @@ -237,9 +383,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", - "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "version": "0.28.1", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha1-RkUs8yHcf56Rwvp4Cla7Vuec1os=", "cpu": [ "loong64" ], @@ -254,9 +400,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", - "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "version": "0.28.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha1-QhGzGE3WYI9T3LIuOfXTTuCIUsg=", "cpu": [ "mips64el" ], @@ -271,9 +417,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", - "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "version": "0.28.1", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha1-aXhXwqYcubC2u2ZS5AwdxeHKjl0=", "cpu": [ "ppc64" ], @@ -288,9 +434,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", - "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "version": "0.28.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha1-0ZKUPrFGpArExkl9DPe+NbmGvwg=", "cpu": [ "riscv64" ], @@ -305,9 +451,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", - "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "version": "0.28.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha1-rOoDVtoODrwI+Xz3ucLkAeHmSNw=", "cpu": [ "s390x" ], @@ -322,9 +468,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", - "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "version": "0.28.1", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha1-bww84MtkxTS3DExF7LLBbTTjXf0=", "cpu": [ "x64" ], @@ -339,9 +485,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", - "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "version": "0.28.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha1-i813B3oNzjN4tXT+2ybSolO3PTY=", "cpu": [ "arm64" ], @@ -356,9 +502,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", - "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "version": "0.28.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha1-5/sqAemcgwyU5mI82f77TI+1g0c=", "cpu": [ "x64" ], @@ -373,9 +519,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", - "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "version": "0.28.1", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha1-xSkJNy24uG4sVeBaiUADO1Zgo7I=", "cpu": [ "arm64" ], @@ -390,9 +536,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", - "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "version": "0.28.1", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha1-xCe5vlpkwmL/mn63C1+7qt9EbGw=", "cpu": [ "x64" ], @@ -407,9 +553,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", - "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "version": "0.28.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha1-3JsUe6yi5sSzyFVxdB70hgpIkJc=", "cpu": [ "arm64" ], @@ -424,9 +570,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", - "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "version": "0.28.1", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha1-zoZtEt8TwV5MmfBzo9Rm9uBkmzo=", "cpu": [ "x64" ], @@ -441,9 +587,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", - "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "version": "0.28.1", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha1-dGjjaS0B1inVlB5dg4F7uA+eObQ=", "cpu": [ "arm64" ], @@ -458,9 +604,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", - "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "version": "0.28.1", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha1-pbwAY/sryrbQ7WPyoVN5WLwmnsY=", "cpu": [ "ia32" ], @@ -475,9 +621,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", - "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "version": "0.28.1", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha1-EAZO5E9DR7kMmgK0Rrv4CpFjKxI=", "cpu": [ "x64" ], @@ -493,8 +639,8 @@ }, "node_modules/@eslint-community/eslint-utils": { "version": "4.9.1", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", - "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha1-TpCvZ7xR3e5s3vUoTt9XLsN2tZU=", "dev": true, "license": "MIT", "dependencies": { @@ -512,8 +658,8 @@ }, "node_modules/@eslint-community/regexpp": { "version": "4.12.2", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", - "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha1-vM32Fbz3tujbgw7AuNIcmiXeWXs=", "dev": true, "license": "MIT", "engines": { @@ -522,8 +668,8 @@ }, "node_modules/@eslint/config-array": { "version": "0.21.2", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", - "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha1-8p4iBXrVMWzyODbO6aNMgf/8t+Y=", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -537,15 +683,15 @@ }, "node_modules/@eslint/config-array/node_modules/balanced-match": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha1-6D46fj8wCzTLnYf2FfoMvzV2kO4=", "dev": true, "license": "MIT" }, "node_modules/@eslint/config-array/node_modules/brace-expansion": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", - "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "version": "1.1.16", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/brace-expansion/-/brace-expansion-1.1.16.tgz", + "integrity": "sha1-cj06MMBVjCJavJ/Eeac+FOJsPC8=", "dev": true, "license": "MIT", "dependencies": { @@ -555,8 +701,8 @@ }, "node_modules/@eslint/config-array/node_modules/minimatch": { "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha1-WAyI+NVEXyvWqo88re+g3nn71p4=", "dev": true, "license": "ISC", "dependencies": { @@ -568,8 +714,8 @@ }, "node_modules/@eslint/config-helpers": { "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", - "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha1-G9AGzut+LlWyt3OrMY0wDhpmrto=", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -581,8 +727,8 @@ }, "node_modules/@eslint/core": { "version": "0.17.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", - "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha1-dyJYIEE9lhdQnak0IZCiAZ54dhw=", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -593,9 +739,9 @@ } }, "node_modules/@eslint/eslintrc": { - "version": "3.3.5", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", - "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==", + "version": "3.3.6", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@eslint/eslintrc/-/eslintrc-3.3.6.tgz", + "integrity": "sha1-0iv9azp9jh8sCy8ubeERtT7G4T4=", "dev": true, "license": "MIT", "dependencies": { @@ -605,7 +751,7 @@ "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", - "js-yaml": "^4.1.1", + "js-yaml": "^4.3.0", "minimatch": "^3.1.5", "strip-json-comments": "^3.1.1" }, @@ -616,17 +762,34 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/@eslint/eslintrc/node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha1-B+mCx0YmFnqnoklcU4F4ktcTlJI=", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, "node_modules/@eslint/eslintrc/node_modules/balanced-match": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha1-6D46fj8wCzTLnYf2FfoMvzV2kO4=", "dev": true, "license": "MIT" }, "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", - "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "version": "1.1.16", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/brace-expansion/-/brace-expansion-1.1.16.tgz", + "integrity": "sha1-cj06MMBVjCJavJ/Eeac+FOJsPC8=", "dev": true, "license": "MIT", "dependencies": { @@ -636,18 +799,25 @@ }, "node_modules/@eslint/eslintrc/node_modules/ignore": { "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha1-PNQOcp82Q/2HywTlC/DrcivFlvU=", "dev": true, "license": "MIT", "engines": { "node": ">= 4" } }, + "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha1-afaofZUTq4u4/mO9sJecRI5oRmA=", + "dev": true, + "license": "MIT" + }, "node_modules/@eslint/eslintrc/node_modules/minimatch": { "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha1-WAyI+NVEXyvWqo88re+g3nn71p4=", "dev": true, "license": "ISC", "dependencies": { @@ -658,9 +828,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.39.4", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz", - "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==", + "version": "9.39.5", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@eslint/js/-/js-9.39.5.tgz", + "integrity": "sha1-by+8/3VQDSKdU14KlJrhNHLIR4c=", "dev": true, "license": "MIT", "engines": { @@ -672,8 +842,8 @@ }, "node_modules/@eslint/object-schema": { "version": "2.1.7", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", - "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha1-biEmoTR+hqTe34cG7Gf/jhB+u60=", "dev": true, "license": "Apache-2.0", "engines": { @@ -682,8 +852,8 @@ }, "node_modules/@eslint/plugin-kit": { "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", - "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha1-l3nj/Zt+4zVxpXQ1z0M1oXlKbLI=", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -694,10 +864,31 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@fontsource-variable/inter": { + "version": "5.2.8", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@fontsource-variable/inter/-/inter-5.2.8.tgz", + "integrity": "sha1-KbEUdvUUn2pEO032UW4mAC2HlBo=", + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, + "node_modules/@hono/node-server": { + "version": "2.0.10", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@hono/node-server/-/node-server-2.0.10.tgz", + "integrity": "sha1-zq0Nl2OasTjC9aOBKJMXY+mSr9U=", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "hono": "^4" + } + }, "node_modules/@humanfs/core": { "version": "0.19.2", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", - "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha1-qCcsoDsqz0kmcCIrIyC2xCG/3mA=", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -709,8 +900,8 @@ }, "node_modules/@humanfs/node": { "version": "0.16.8", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", - "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha1-j4AMzME/T4zTEW4tnAqUk52j4+0=", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -724,8 +915,8 @@ }, "node_modules/@humanfs/types": { "version": "0.15.0", - "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", - "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha1-8qCfYgEjkLK/8/xvskjd7IwJoJA=", "dev": true, "license": "Apache-2.0", "engines": { @@ -734,8 +925,8 @@ }, "node_modules/@humanwhocodes/module-importer": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha1-r1smkaIrRL6EewyoFkHF+2rQFyw=", "dev": true, "license": "Apache-2.0", "engines": { @@ -748,8 +939,8 @@ }, "node_modules/@humanwhocodes/retry": { "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", - "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha1-wrnS43TuYsWG062+qHGZsdenpro=", "dev": true, "license": "Apache-2.0", "engines": { @@ -762,8 +953,8 @@ }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha1-Y0Khn0Q0dRjJPkOxrGnes8Rlah8=", "dev": true, "license": "MIT", "dependencies": { @@ -773,8 +964,8 @@ }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha1-eg7mAfYPmaIMfHxf8MgDiMEYm9Y=", "dev": true, "license": "MIT", "engines": { @@ -783,15 +974,15 @@ }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha1-aRKwDSxjHA0Vzhp6tXzWV/Ko+Lo=", "dev": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha1-2xXWeByTHzolGj2sOVAcmKYIL9A=", "dev": true, "license": "MIT", "dependencies": { @@ -799,290 +990,619 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.3.tgz", - "integrity": "sha512-x35CNW/ANXG3hE/EZpRU8MXX1JDN86hBb2wMGAtltkz7pc6cxgjpy1OMMfDosOQ+2hWqIkag/fGok1Yady9nGw==", - "cpu": [ - "arm" + "node_modules/@kurkle/color": { + "version": "0.3.4", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@kurkle/color/-/color-0.3.4.tgz", + "integrity": "sha1-TU/2d+FgkhT8ccWAEl3d3Yaryr8=", + "license": "MIT" + }, + "node_modules/@lezer/common": { + "version": "1.5.2", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@lezer/common/-/common-1.5.2.tgz", + "integrity": "sha1-1oQNsTd54/G0LnDJqXxAhtEvriI=", + "license": "MIT" + }, + "node_modules/@lezer/highlight": { + "version": "1.2.3", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@lezer/highlight/-/highlight-1.2.3.tgz", + "integrity": "sha1-og8yS3EUii6pum/0Lli7+uxwKFc=", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.3.0" + } + }, + "node_modules/@lezer/json": { + "version": "1.0.3", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@lezer/json/-/json-1.0.3.tgz", + "integrity": "sha1-53OgEq0AiPvwfOSc+6h1zJ5bwF8=", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@lezer/lr": { + "version": "1.4.10", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@lezer/lr/-/lr-1.4.10.tgz", + "integrity": "sha1-s6zDblrQSbdN23cZWU5+dNkWH/U=", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@marijn/find-cluster-break": { + "version": "1.0.3", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@marijn/find-cluster-break/-/find-cluster-break-1.0.3.tgz", + "integrity": "sha1-vb6QfgDBfITjP8UbFRnZqiEefo4=", + "license": "MIT" + }, + "node_modules/@modelcontextprotocol/ext-apps": { + "version": "1.7.4", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@modelcontextprotocol/ext-apps/-/ext-apps-1.7.4.tgz", + "integrity": "sha1-i5cAWhNKbakM4Ii/2iCXRkvtm0Y=", + "license": "MIT", + "workspaces": [ + "examples/*" ], - "dev": true, + "dependencies": { + "@standard-schema/spec": "^1.1.0" + }, + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "@modelcontextprotocol/sdk": "^1.29.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0", + "zod": "^3.25.0 || ^4.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.29.0", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@modelcontextprotocol/sdk/-/sdk-1.29.0.tgz", + "integrity": "sha1-eXhti1JeJp3oUKyCsfH3V/ORX0Q=", "license": "MIT", - "optional": true, - "os": [ - "android" - ] + "dependencies": { + "@hono/node-server": "^1.19.9", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.2.1", + "express-rate-limit": "^8.2.1", + "hono": "^4.11.4", + "jose": "^6.1.3", + "json-schema-typed": "^8.0.2", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.25 || ^4.0", + "zod-to-json-schema": "^3.25.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@cfworker/json-schema": "^4.1.1", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "@cfworker/json-schema": { + "optional": true + }, + "zod": { + "optional": false + } + } }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.3.tgz", - "integrity": "sha512-xw3xtkDApIOGayehp2+Rz4zimfkaX65r4t47iy+ymQB2G4iJCBBfj0ogVg5jpvjpn8UWn/+q9tprxleYeNp3Hw==", + "node_modules/@napi-rs/canvas": { + "version": "1.0.2", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@napi-rs/canvas/-/canvas-1.0.2.tgz", + "integrity": "sha1-YkMd86TIdQBuGYl5vrioc8vKDrA=", + "license": "MIT", + "workspaces": [ + "e2e/*" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "optionalDependencies": { + "@napi-rs/canvas-android-arm64": "1.0.2", + "@napi-rs/canvas-darwin-arm64": "1.0.2", + "@napi-rs/canvas-darwin-x64": "1.0.2", + "@napi-rs/canvas-linux-arm-gnueabihf": "1.0.2", + "@napi-rs/canvas-linux-arm64-gnu": "1.0.2", + "@napi-rs/canvas-linux-arm64-musl": "1.0.2", + "@napi-rs/canvas-linux-riscv64-gnu": "1.0.2", + "@napi-rs/canvas-linux-x64-gnu": "1.0.2", + "@napi-rs/canvas-linux-x64-musl": "1.0.2", + "@napi-rs/canvas-win32-arm64-msvc": "1.0.2", + "@napi-rs/canvas-win32-x64-msvc": "1.0.2" + } + }, + "node_modules/@napi-rs/canvas-android-arm64": { + "version": "1.0.2", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-1.0.2.tgz", + "integrity": "sha1-jUVdBqjygqSRi/Ez3Qhdupt0YGI=", "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ "android" - ] + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.3.tgz", - "integrity": "sha512-vo6Y5Qfpx7/5EaamIwi0WqW2+zfiusVihKatLvtN1VFVy3D13uERk/6gZLU1UiHRL6fDXqj/ELIeVRGnvcTE1g==", + "node_modules/@napi-rs/canvas-darwin-arm64": { + "version": "1.0.2", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-1.0.2.tgz", + "integrity": "sha1-n0znqx+bwQVjinQydK8DVxv1bP8=", "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ "darwin" - ] + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.3.tgz", - "integrity": "sha512-D+0QGcZhBzTN82weOnsSlY7V7+RMmPuF1CkbxyMAGE8+ZHeUjyb76ZiWmBlCu//AQQONvxcqRbwZTajZKqjuOw==", + "node_modules/@napi-rs/canvas-darwin-x64": { + "version": "1.0.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-1.0.2.tgz", + "integrity": "sha1-X/mTWFb6NAnLAvoXzYUwBeoffYg=", "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ "darwin" - ] + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.3.tgz", - "integrity": "sha512-6HnvHCT7fDyj6R0Ph7A6x8dQS/S38MClRWeDLqc0MdfWkxjiu1HSDYrdPhqSILzjTIC/pnXbbJbo+ft+gy/9hQ==", + "node_modules/@napi-rs/canvas-linux-arm-gnueabihf": { + "version": "1.0.2", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-1.0.2.tgz", + "integrity": "sha1-++4fOk1qJ/G+VTmUh8K/NCucO7Y=", "cpu": [ - "arm64" + "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ - "freebsd" - ] + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.3.tgz", - "integrity": "sha512-KHLgC3WKlUYW3ShFKnnosZDOJ0xjg9zp7au3sIm2bs/tGBeC2ipmvRh/N7JKi0t9Ue20C0dpEshi8WUubg+cnA==", + "node_modules/@napi-rs/canvas-linux-arm64-gnu": { + "version": "1.0.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-1.0.2.tgz", + "integrity": "sha1-ZDLLhhNwQ38LEgb+Re0MvUyjK/Q=", "cpu": [ - "x64" + "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ - "freebsd" - ] + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.3.tgz", - "integrity": "sha512-DV6fJoxEYWJOvaZIsok7KrYl0tPvga5OZ2yvKHNNYyk/2roMLqQAbGhr78EQ5YhHpnhLKJD3S1WFusAkmUuV5g==", + "node_modules/@napi-rs/canvas-linux-arm64-musl": { + "version": "1.0.2", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-1.0.2.tgz", + "integrity": "sha1-ZURXdDYrmQGVZlziwUYFo0LIOIc=", "cpu": [ - "arm" + "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.3.tgz", - "integrity": "sha512-mQKoJAzvuOs6F+TZybQO4GOTSMUu7v0WdxEk24krQ/uUxXoPTtHjuaUuPmFhtBcM4K0ons8nrE3JyhTuCFtT/w==", + "node_modules/@napi-rs/canvas-linux-riscv64-gnu": { + "version": "1.0.2", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-1.0.2.tgz", + "integrity": "sha1-ULJ+W7f/2vjauz423K4uuIKz2TQ=", "cpu": [ - "arm" + "riscv64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.3.tgz", - "integrity": "sha512-Whjj2qoiJ6+OOJMGptTYazaJvjOJm+iKHpXQM1P3LzGjt7Ff++Tp7nH4N8J/BUA7R9IHfDyx4DJIflifwnbmIA==", + "node_modules/@napi-rs/canvas-linux-x64-gnu": { + "version": "1.0.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-1.0.2.tgz", + "integrity": "sha1-gOfVc+oDAdSOXeMM2YPb132oiNE=", "cpu": [ - "arm64" + "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.3.tgz", - "integrity": "sha512-4YTNHKqGng5+yiZt3mg77nmyuCfmNfX4fPmyUapBcIk+BdwSwmCWGXOUxhXbBEkFHtoN5boLj/5NON+u5QC9tg==", + "node_modules/@napi-rs/canvas-linux-x64-musl": { + "version": "1.0.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-1.0.2.tgz", + "integrity": "sha1-n+OlDOwJMc0vIA2Ke6yR2LDd1aA=", "cpu": [ - "arm64" + "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } }, - "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.3.tgz", - "integrity": "sha512-SU3kNlhkpI4UqlUc2VXPGK9o886ZsSeGfMAX2ba2b8DKmMXq4AL7KUrkSWVbb7koVqx41Yczx6dx5PNargIrEA==", + "node_modules/@napi-rs/canvas-win32-arm64-msvc": { + "version": "1.0.2", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@napi-rs/canvas-win32-arm64-msvc/-/canvas-win32-arm64-msvc-1.0.2.tgz", + "integrity": "sha1-0w4oQfM2Av+ev3L8Aovu7hV398g=", "cpu": [ - "loong64" + "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ - "linux" - ] + "win32" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } }, - "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.3.tgz", - "integrity": "sha512-6lDLl5h4TXpB1mTf2rQWnAk/LcXrx9vBfu/DT5TIPhvMhRWaZ5MxkIc8u4lJAmBo6klTe1ywXIUHFjylW505sg==", + "node_modules/@napi-rs/canvas-win32-x64-msvc": { + "version": "1.0.2", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-1.0.2.tgz", + "integrity": "sha1-hhul5Ib/GqOB8yXCfOiM+fI3BiY=", "cpu": [ - "loong64" + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.6", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", + "integrity": "sha1-7TOAbQ+b6Y3HbQw9T9hy/acBtdU=", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.3" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.139.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@oxc-project/types/-/types-0.139.0.tgz", + "integrity": "sha1-ONdrnb+TTCoCvhdPsyzuvxgv50I=", "dev": true, "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@resvg/resvg-js": { + "version": "2.6.2", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@resvg/resvg-js/-/resvg-js-2.6.2.tgz", + "integrity": "sha1-PpKpB9iNh5JWxYU0fFshp/O7W0Y=", + "license": "MPL-2.0", + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@resvg/resvg-js-android-arm-eabi": "2.6.2", + "@resvg/resvg-js-android-arm64": "2.6.2", + "@resvg/resvg-js-darwin-arm64": "2.6.2", + "@resvg/resvg-js-darwin-x64": "2.6.2", + "@resvg/resvg-js-linux-arm-gnueabihf": "2.6.2", + "@resvg/resvg-js-linux-arm64-gnu": "2.6.2", + "@resvg/resvg-js-linux-arm64-musl": "2.6.2", + "@resvg/resvg-js-linux-x64-gnu": "2.6.2", + "@resvg/resvg-js-linux-x64-musl": "2.6.2", + "@resvg/resvg-js-win32-arm64-msvc": "2.6.2", + "@resvg/resvg-js-win32-ia32-msvc": "2.6.2", + "@resvg/resvg-js-win32-x64-msvc": "2.6.2" + } + }, + "node_modules/@resvg/resvg-js-android-arm-eabi": { + "version": "2.6.2", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@resvg/resvg-js-android-arm-eabi/-/resvg-js-android-arm-eabi-2.6.2.tgz", + "integrity": "sha1-52HgtogSfbZIefRVF4ySRoqa6r4=", + "cpu": [ + "arm" + ], + "license": "MPL-2.0", "optional": true, "os": [ - "linux" - ] + "android" + ], + "engines": { + "node": ">= 10" + } }, - "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.3.tgz", - "integrity": "sha512-BMo8bOw8evlup/8G+cj5xWtPyp93xPdyoSN16Zy90Q2QZ0ZYRhCt6ZJSwbrRzG9HApFabjwj2p25TUPDWrhzqQ==", + "node_modules/@resvg/resvg-js-android-arm64": { + "version": "2.6.2", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@resvg/resvg-js-android-arm64/-/resvg-js-android-arm64-2.6.2.tgz", + "integrity": "sha1-uMtWTX9rPzfZtDEp9dxf4XHiSeQ=", "cpu": [ - "ppc64" + "arm64" ], - "dev": true, - "license": "MIT", + "license": "MPL-2.0", "optional": true, "os": [ - "linux" - ] + "android" + ], + "engines": { + "node": ">= 10" + } }, - "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.3.tgz", - "integrity": "sha512-E0L8X1dZN1/Rph+5VPF6Xj2G7JJvMACVXtamTJIDrVI44Y3K+G8gQaMEAavbqCGTa16InptiVrX6eM6pmJ+7qA==", + "node_modules/@resvg/resvg-js-darwin-arm64": { + "version": "2.6.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@resvg/resvg-js-darwin-arm64/-/resvg-js-darwin-arm64-2.6.2.tgz", + "integrity": "sha1-Sb0/rtpcSfUzAtlw5uedAG3hjn0=", "cpu": [ - "ppc64" + "arm64" ], - "dev": true, - "license": "MIT", + "license": "MPL-2.0", "optional": true, "os": [ - "linux" - ] + "darwin" + ], + "engines": { + "node": ">= 10" + } }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.3.tgz", - "integrity": "sha512-oZJ/WHaVfHUiRAtmTAeo3DcevNsVvH8mbvodjZy7D5QKvCefO371SiKRpxoDcCxB3PTRTLayWBkvmDQKTcX/sw==", + "node_modules/@resvg/resvg-js-darwin-x64": { + "version": "2.6.2", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@resvg/resvg-js-darwin-x64/-/resvg-js-darwin-x64-2.6.2.tgz", + "integrity": "sha1-4TRBc6onv7TYgKtXbRrPHBZI+so=", "cpu": [ - "riscv64" + "x64" ], - "dev": true, - "license": "MIT", + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@resvg/resvg-js-linux-arm-gnueabihf": { + "version": "2.6.2", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@resvg/resvg-js-linux-arm-gnueabihf/-/resvg-js-linux-arm-gnueabihf-2.6.2.tgz", + "integrity": "sha1-NMRF66Re/Wj2EwsqtCbXanQkJT0=", + "cpu": [ + "arm" + ], + "license": "MPL-2.0", "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": ">= 10" + } }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.3.tgz", - "integrity": "sha512-Dhbyh7j9FybM3YaTgaHmVALwA8AkUwTPccyCQ79TG9AJUsMQqgN1DDEZNr4+QUfwiWvLDumW5vdwzoeUF+TNxQ==", + "node_modules/@resvg/resvg-js-linux-arm64-gnu": { + "version": "2.6.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@resvg/resvg-js-linux-arm64-gnu/-/resvg-js-linux-arm64-gnu-2.6.2.tgz", + "integrity": "sha1-MNpHCH3YFTGCGYuU/p+NmUiQ2uU=", "cpu": [ - "riscv64" + "arm64" ], - "dev": true, - "license": "MIT", + "license": "MPL-2.0", "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": ">= 10" + } }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.3.tgz", - "integrity": "sha512-cJd1X5XhHHlltkaypz1UcWLA8AcoIi1aWhsvaWDskD1oz2eKCypnqvTQ8ykMNI0RSmm7NkTdSqSSD7zM0xa6Ig==", + "node_modules/@resvg/resvg-js-linux-arm64-musl": { + "version": "2.6.2", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@resvg/resvg-js-linux-arm64-musl/-/resvg-js-linux-arm64-musl-2.6.2.tgz", + "integrity": "sha1-XXW4/1yDEDcpwco3eZhzAnU8UNQ=", "cpu": [ - "s390x" + "arm64" ], - "dev": true, - "license": "MIT", + "license": "MPL-2.0", "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": ">= 10" + } }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.3.tgz", - "integrity": "sha512-DAZDBHQfG2oQuhY7mc6I3/qB4LU2fQCjRvxbDwd/Jdvb9fypP4IJ4qmtu6lNjes6B531AI8cg1aKC2di97bUxA==", + "node_modules/@resvg/resvg-js-linux-x64-gnu": { + "version": "2.6.2", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@resvg/resvg-js-linux-x64-gnu/-/resvg-js-linux-x64-gnu-2.6.2.tgz", + "integrity": "sha1-QRq+367l7cV8u3cBc2zsulIuJvM=", "cpu": [ "x64" ], - "dev": true, - "license": "MIT", + "license": "MPL-2.0", "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": ">= 10" + } }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.3.tgz", - "integrity": "sha512-cRxsE8c13mZOh3vP+wLDxpQBRrOHDIGOWyDL93Sy0Ga8y515fBcC2pjUfFwUe5T7tqvTvWbCpg1URM/AXdWIXA==", + "node_modules/@resvg/resvg-js-linux-x64-musl": { + "version": "2.6.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@resvg/resvg-js-linux-x64-musl/-/resvg-js-linux-x64-musl-2.6.2.tgz", + "integrity": "sha1-/kmEA48DcvJ54/9XC3KTTdfrKlw=", "cpu": [ "x64" ], - "dev": true, - "license": "MIT", + "license": "MPL-2.0", "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": ">= 10" + } }, - "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.3.tgz", - "integrity": "sha512-QaWcIgRxqEdQdhJqW4DJctsH6HCmo5vHxY0krHSX4jMtOqfzC+dqDGuHM87bu4H8JBeibWx7jFz+h6/4C8wA5Q==", + "node_modules/@resvg/resvg-js-win32-arm64-msvc": { + "version": "2.6.2", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@resvg/resvg-js-win32-arm64-msvc/-/resvg-js-win32-arm64-msvc-2.6.2.tgz", + "integrity": "sha1-06BTz3/2hwh6IQYzDA/arnBiVNE=", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@resvg/resvg-js-win32-ia32-msvc": { + "version": "2.6.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@resvg/resvg-js-win32-ia32-msvc/-/resvg-js-win32-ia32-msvc-2.6.2.tgz", + "integrity": "sha1-fN2hzinvcgnigZHZF/pb7wYkpK0=", + "cpu": [ + "ia32" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@resvg/resvg-js-win32-x64-msvc": { + "version": "2.6.2", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@resvg/resvg-js-win32-x64-msvc/-/resvg-js-win32-x64-msvc-2.6.2.tgz", + "integrity": "sha1-ywrQRSXWXz3vTI00YVeleXbVs4g=", "cpu": [ "x64" ], - "dev": true, - "license": "MIT", + "license": "MPL-2.0", "optional": true, "os": [ - "openbsd" - ] + "win32" + ], + "engines": { + "node": ">= 10" + } }, - "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.3.tgz", - "integrity": "sha512-AaXwSvUi3QIPtroAUw1t5yHGIyqKEXwH54WUocFolZhpGDruJcs8c+xPNDRn4XiQsS7MEwnYsHW2l0MBLDMkWg==", + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.1.5", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.5.tgz", + "integrity": "sha1-9Yy5oKgSjtBYIoJyBShUf8XANfM=", "cpu": [ "arm64" ], @@ -1090,13 +1610,16 @@ "license": "MIT", "optional": true, "os": [ - "openharmony" - ] + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.3.tgz", - "integrity": "sha512-65LAKM/bAWDqKNEelHlcHvm2V+Vfb8C6INFxQXRHCvaVN1rJfwr4NvdP4FyzUaLqWfaCGaadf6UbTm8xJeYfEg==", + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.1.5", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.5.tgz", + "integrity": "sha1-RBFEwFpKgxqnUmmrw6SjJDdOpwc=", "cpu": [ "arm64" ], @@ -1104,27 +1627,33 @@ "license": "MIT", "optional": true, "os": [ - "win32" - ] + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.3.tgz", - "integrity": "sha512-EEM2gyhBF5MFnI6vMKdX1LAosE627RGBzIoGMdLloPZkXrUN0Ckqgr2Qi8+J3zip/8NVVro3/FjB+tjhZUgUHA==", + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.1.5", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.5.tgz", + "integrity": "sha1-yC4wZSzvUsSvkl1cZsiVWkAxmBY=", "cpu": [ - "ia32" + "x64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "win32" - ] + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.3.tgz", - "integrity": "sha512-E5Eb5H/DpxaoXH++Qkv28RcUJboMopmdDUALBczvHMf7hNIxaDZqwY5lK12UK1BHacSmvupoEWGu+n993Z0y1A==", + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.1.5", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.5.tgz", + "integrity": "sha1-wy6c5/ocD7K4CROio6BcPpB9BrA=", "cpu": [ "x64" ], @@ -1132,2830 +1661,7776 @@ "license": "MIT", "optional": true, "os": [ - "win32" - ] + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.3.tgz", - "integrity": "sha512-hPt/bgL5cE+Qp+/TPHBqptcAgPzgj46mPcg/16zNUmbQk0j+mOEQV/+Lqu8QRtDV3Ek95Q6FeFITpuhl6OTsAA==", + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.1.5", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.5.tgz", + "integrity": "sha1-zpC14iMWretQLqAQWC9JjNBgTyc=", "cpu": [ - "x64" + "arm" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "win32" - ] + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@types/estree": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", - "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.1.5", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.5.tgz", + "integrity": "sha1-kZRxEMTdqk7vsATlJogHCXcIUgE=", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "20.19.41", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.41.tgz", - "integrity": "sha512-ECymXOukMnOoVkC2bb1Vc/w/836DXncOg5m8Xj1RH7xSHZJWNYY6Zh7EH477vcnD5egKNNfy2RpNOmuChhFPgQ==", + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.1.5", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.5.tgz", + "integrity": "sha1-6zKy1BCMHHArkejN6KBD6uXKqU0=", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "undici-types": "~6.21.0" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.59.3", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.3.tgz", - "integrity": "sha512-PwFvSKsXGShKGW6n5bZOhGHEcCZXM8HofLK9fNsEwZXzFRjoY+XT1Vsf1zgyXdwTr0ZYz1/2tkZ0DBTT9jZjhw==", + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.1.5", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.5.tgz", + "integrity": "sha1-zLlDwR5acmVcuwL8EWNUHOtkB4I=", + "cpu": [ + "ppc64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.59.3", - "@typescript-eslint/type-utils": "8.59.3", - "@typescript-eslint/utils": "8.59.3", - "@typescript-eslint/visitor-keys": "8.59.3", - "ignore": "^7.0.5", - "natural-compare": "^1.4.0", - "ts-api-utils": "^2.5.0" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "@typescript-eslint/parser": "^8.59.3", - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@typescript-eslint/parser": { - "version": "8.59.3", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.59.3.tgz", - "integrity": "sha512-HPwA+hVkfcriajbNvTmZv4VRauibay+cWArYUYq7u7W7PmGShMxbPxLvrwDme55a6d5alG3nrYfhyJ/G28XlLg==", + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.1.5", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.5.tgz", + "integrity": "sha1-ozcx7lZ+kLdfrG5eVTB+iiswOPA=", + "cpu": [ + "s390x" + ], "dev": true, "license": "MIT", - "dependencies": { - "@typescript-eslint/scope-manager": "8.59.3", - "@typescript-eslint/types": "8.59.3", - "@typescript-eslint/typescript-estree": "8.59.3", - "@typescript-eslint/visitor-keys": "8.59.3", - "debug": "^4.4.3" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@typescript-eslint/project-service": { - "version": "8.59.3", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.59.3.tgz", - "integrity": "sha512-ECiUWa/KYRGDFUqTNehaRgzDshnJfkTABJxVemHk4ko22gcr0ukloKjWvyQ64g8YCV/UI47kN1dbmjf/GaQYng==", + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.1.5", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.5.tgz", + "integrity": "sha1-p0wBqqzt/BHDm2/rozpfoMZUlJ8=", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.59.3", - "@typescript-eslint/types": "^8.59.3", - "debug": "^4.4.3" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.1.0" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "8.59.3", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.3.tgz", - "integrity": "sha512-t2LvZnoEfzKtnPjgeEu41xw5gxq9mQVfYy4OoZ4Vlt0sk3JwxmhCca/AR7DwOiHrjWgjAj6as4AhRLKSDfvZIA==", + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.1.5", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.5.tgz", + "integrity": "sha1-KM0XhJT6HmXbpBIim1zlXE3Vy9E=", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.59.3", - "@typescript-eslint/visitor-keys": "8.59.3" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.59.3", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.3.tgz", - "integrity": "sha512-PcIJHjmaREXLgIAIzLnSY9VucEzz8FKXsRgFa1DmdGCK/5tJpW03TKJF01Q6VZd1lLdz2sIKPWaDUZN9dp//dw==", + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.1.5", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.5.tgz", + "integrity": "sha1-98dfqRP8IIhNJqfUiNT1xZfNccA=", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.1.0" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@typescript-eslint/type-utils": { - "version": "8.59.3", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.59.3.tgz", - "integrity": "sha512-g71d8QD8UaiHGvrJwyIS1hCX5r63w6Jll+4VEYhEAHXTDIqX1JgxhTAbEHtKntL9kuc4jRo7/GWw5xfCepSccQ==", + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.1.5", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.5.tgz", + "integrity": "sha1-w3lYGUd4cIHfNj6hBhQPzV/sJS0=", + "cpu": [ + "wasm32" + ], "dev": true, "license": "MIT", + "optional": true, "dependencies": { - "@typescript-eslint/types": "8.59.3", - "@typescript-eslint/typescript-estree": "8.59.3", - "@typescript-eslint/utils": "8.59.3", - "debug": "^4.4.3", - "ts-api-utils": "^2.5.0" + "@emnapi/core": "1.11.1", + "@emnapi/runtime": "1.11.1", + "@napi-rs/wasm-runtime": "^1.1.6" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@typescript-eslint/types": { - "version": "8.59.3", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.3.tgz", - "integrity": "sha512-ePFoH0g4ludssdRFqqDxQePCxU4WQyRa9+XVwjm7yLn0FKhMeoetC+qBEEI1Eyb1pGSDveTIT09Bvw2WhlGayg==", + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.1.5", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.5.tgz", + "integrity": "sha1-8jyIaU96cpoS85UCSu4434Cha6M=", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.59.3", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.3.tgz", - "integrity": "sha512-CbRjVRAf7Lr9Kr8RopKcbY45p2VfmmHrm0ygOCYFi7oU8q19m0Fs/6iHS7kNOmwpp+ob07ZVcAqlxUod9lYdmg==", + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.1.5", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.5.tgz", + "integrity": "sha1-M3fI3g5WqIV/ESF1YRRwkOh0y0Y=", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@typescript-eslint/project-service": "8.59.3", - "@typescript-eslint/tsconfig-utils": "8.59.3", - "@typescript-eslint/types": "8.59.3", - "@typescript-eslint/visitor-keys": "8.59.3", - "debug": "^4.4.3", - "minimatch": "^10.2.2", - "semver": "^7.7.3", - "tinyglobby": "^0.2.15", - "ts-api-utils": "^2.5.0" - }, + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.1.0" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@typescript-eslint/utils": { - "version": "8.59.3", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.59.3.tgz", - "integrity": "sha512-JAvT14goBzRzzzZyqq3P9BLArIxTtQURUtFgQ/V7FO+eU+Gg6ES+5ymOPP1wRxXcxAYeivCk4uS3jCKWI1K8Zg==", + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha1-4/zuCT+7XOdl4a0Ij/TeKIn2+b4=", "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.59.3", - "@typescript-eslint/types": "8.59.3", - "@typescript-eslint/typescript-estree": "8.59.3" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" - } + "license": "MIT" }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.59.3", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.3.tgz", - "integrity": "sha512-f1UQF7ggd42YiwI5wGrRaPsa+P0CINBlrkLPmGfpq/u/I/oVtecoEIfFR9ag/oa1sLOsRNZ6xehf6qMZhQGBDg==", + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.2", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", + "integrity": "sha1-XphJtmHCIpz5Z6CNvi276ejJkeU=", + "cpu": [ + "arm" + ], "dev": true, "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.59.3", - "eslint-visitor-keys": "^5.0.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } + "optional": true, + "os": [ + "android" + ] }, - "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", - "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", + "integrity": "sha1-WwaZ7l3UhLIiye10r/Q8keqLF/g=", + "cpu": [ + "arm64" + ], "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } + "license": "MIT", + "optional": true, + "os": [ + "android" + ] }, - "node_modules/@vitest/expect": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.9.tgz", - "integrity": "sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==", + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.2", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", + "integrity": "sha1-i8UsnXo86NBTPDUanJNd54HaoG8=", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@vitest/spy": "2.1.9", - "@vitest/utils": "2.1.9", - "chai": "^5.1.2", - "tinyrainbow": "^1.2.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } + "optional": true, + "os": [ + "darwin" + ] }, - "node_modules/@vitest/mocker": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.9.tgz", - "integrity": "sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==", + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.2", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz", + "integrity": "sha1-ui7z6PsxDwrzVYjycM+lqpbkh2Q=", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@vitest/spy": "2.1.9", - "estree-walker": "^3.0.3", - "magic-string": "^0.30.12" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "msw": "^2.4.9", - "vite": "^5.0.0" - }, - "peerDependenciesMeta": { - "msw": { - "optional": true - }, - "vite": { - "optional": true - } - } + "optional": true, + "os": [ + "darwin" + ] }, - "node_modules/@vitest/pretty-format": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.9.tgz", - "integrity": "sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==", + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz", + "integrity": "sha1-k7EL2/6K2iJri8DALva39URHTZY=", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "tinyrainbow": "^1.2.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } + "optional": true, + "os": [ + "freebsd" + ] }, - "node_modules/@vitest/runner": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.9.tgz", - "integrity": "sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==", + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz", + "integrity": "sha1-PoqjjvPJwwCUaHHj/bsMMOCiD4Y=", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@vitest/utils": "2.1.9", - "pathe": "^1.1.2" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/runner/node_modules/pathe": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", - "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", - "dev": true, - "license": "MIT" + "optional": true, + "os": [ + "freebsd" + ] }, - "node_modules/@vitest/snapshot": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.9.tgz", - "integrity": "sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==", + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz", + "integrity": "sha1-HXmUOEuwrRvEGSG1BuFkLU+df8M=", + "cpu": [ + "arm" + ], "dev": true, "license": "MIT", - "dependencies": { - "@vitest/pretty-format": "2.1.9", - "magic-string": "^0.30.12", - "pathe": "^1.1.2" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/snapshot/node_modules/pathe": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", - "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", - "dev": true, - "license": "MIT" + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@vitest/spy": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.9.tgz", - "integrity": "sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==", + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.2", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz", + "integrity": "sha1-plQPR8+ESla4DKn/ldKs37LO+Xs=", + "cpu": [ + "arm" + ], "dev": true, "license": "MIT", - "dependencies": { - "tinyspy": "^3.0.2" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@vitest/utils": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.9.tgz", - "integrity": "sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==", + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", + "integrity": "sha1-QE8gRWUYQMv0jakbptD0kPC8LL8=", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@vitest/pretty-format": "2.1.9", - "loupe": "^3.1.2", - "tinyrainbow": "^1.2.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/acorn": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", - "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz", + "integrity": "sha1-o0BP/d97R0tIyZuciTtiR7t2W6U=", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", + "integrity": "sha1-6KrG1Umzd5ReNJiC8Zm3yOt1yjg=", + "cpu": [ + "loong64" + ], "dev": true, "license": "MIT", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/ajv": { - "version": "6.15.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", - "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.2", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", + "integrity": "sha1-bi5E6lAxCzpYIHipFeX+uHnIINQ=", + "cpu": [ + "loong64" + ], "dev": true, "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", + "integrity": "sha1-aJgwLabXegU3zeZLK0xrYGWb0RA=", + "cpu": [ + "ppc64" + ], "dev": true, "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/any-promise": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", - "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.2", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz", + "integrity": "sha1-MzcXyV3Vpmvvj2Pn74qf2EX9GNA=", + "cpu": [ + "ppc64" + ], "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.2", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz", + "integrity": "sha1-gbwGujgDUgBNAfSCbrfNzO+gW60=", + "cpu": [ + "riscv64" + ], "dev": true, - "license": "Python-2.0" + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/assertion-error": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", - "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.2", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz", + "integrity": "sha1-lafNOd4hOJrWeIpShOqqc44pykw=", + "cpu": [ + "riscv64" + ], "dev": true, "license": "MIT", - "engines": { - "node": ">=12" - } + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.2", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz", + "integrity": "sha1-BubbLsG8SLU3THkj74PC6wJLJFI=", + "cpu": [ + "s390x" + ], "dev": true, "license": "MIT", - "engines": { - "node": "18 || 20 || >=22" - } + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/brace-expansion": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", - "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", - "dev": true, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", + "integrity": "sha1-XcgYmIKF4J6IeQxkYt73JBPfLaM=", + "cpu": [ + "x64" + ], "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - } + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/bundle-require": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/bundle-require/-/bundle-require-5.1.0.tgz", - "integrity": "sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==", + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", + "integrity": "sha1-IID0qTNJ6a/TS+b8GjfgH8i/yA8=", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "load-tsconfig": "^0.2.3" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "peerDependencies": { - "esbuild": ">=0.18" - } + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/cac": { - "version": "6.7.14", - "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", - "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.2", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz", + "integrity": "sha1-IdZKistmIhckuSPlGvUzPfGvBEs=", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "engines": { - "node": ">=8" - } + "optional": true, + "os": [ + "openbsd" + ] }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.2", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz", + "integrity": "sha1-jg/NnQIUHjN7TFtc/1dsuadrG6A=", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "engines": { - "node": ">=6" - } + "optional": true, + "os": [ + "openharmony" + ] }, - "node_modules/chai": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", - "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.2", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz", + "integrity": "sha1-vbTMTv1Y7+gIIDNH8PVGPw6hblI=", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "assertion-error": "^2.0.1", - "check-error": "^2.1.1", - "deep-eql": "^5.0.1", - "loupe": "^3.1.0", - "pathval": "^2.0.0" - }, - "engines": { - "node": ">=18" - } + "optional": true, + "os": [ + "win32" + ] }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.2", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz", + "integrity": "sha1-26695a/STq4O7+kV2QFjLny1mGA=", + "cpu": [ + "ia32" + ], "dev": true, "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } + "optional": true, + "os": [ + "win32" + ] }, - "node_modules/check-error": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", - "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.2", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", + "integrity": "sha1-hBCehf6l+PE1NJn5ZXj9wqDosTg=", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "engines": { - "node": ">= 16" - } + "optional": true, + "os": [ + "win32" + ] }, - "node_modules/chokidar": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", - "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.2", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", + "integrity": "sha1-NnHOP5uSjVwB+Hl5LVwLYK4U1K0=", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha1-p5tV26+GBIEvUtFAssmrQbwVC7g=", + "license": "MIT" + }, + "node_modules/@swc/core": { + "version": "1.15.43", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@swc/core/-/core-1.15.43.tgz", + "integrity": "sha1-ZT5lc5aP1cdBY7mIXqCpMwEsnyI=", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", "dependencies": { - "readdirp": "^4.0.1" + "@swc/counter": "^0.1.3", + "@swc/types": "^0.1.27" }, "engines": { - "node": ">= 14.16.0" + "node": ">=10" }, "funding": { - "url": "https://paulmillr.com/funding/" + "type": "opencollective", + "url": "https://opencollective.com/swc" + }, + "optionalDependencies": { + "@swc/core-darwin-arm64": "1.15.43", + "@swc/core-darwin-x64": "1.15.43", + "@swc/core-linux-arm-gnueabihf": "1.15.43", + "@swc/core-linux-arm64-gnu": "1.15.43", + "@swc/core-linux-arm64-musl": "1.15.43", + "@swc/core-linux-ppc64-gnu": "1.15.43", + "@swc/core-linux-s390x-gnu": "1.15.43", + "@swc/core-linux-x64-gnu": "1.15.43", + "@swc/core-linux-x64-musl": "1.15.43", + "@swc/core-win32-arm64-msvc": "1.15.43", + "@swc/core-win32-ia32-msvc": "1.15.43", + "@swc/core-win32-x64-msvc": "1.15.43" + }, + "peerDependencies": { + "@swc/helpers": ">=0.5.17" + }, + "peerDependenciesMeta": { + "@swc/helpers": { + "optional": true + } } }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "node_modules/@swc/core-darwin-arm64": { + "version": "1.15.43", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@swc/core-darwin-arm64/-/core-darwin-arm64-1.15.43.tgz", + "integrity": "sha1-OGKU+EJ93i3xpw3QpYJtZ69w6ZY=", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">=7.0.0" + "node": ">=10" } }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "node_modules/@swc/core-darwin-x64": { + "version": "1.15.43", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@swc/core-darwin-x64/-/core-darwin-x64-1.15.43.tgz", + "integrity": "sha1-xII1KcQk4q4lt+t4ZDhHR0FSH8s=", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT" + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } }, - "node_modules/commander": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", - "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "node_modules/@swc/core-linux-arm-gnueabihf": { + "version": "1.15.43", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.15.43.tgz", + "integrity": "sha1-wKDtF8/8XUrxkpNWZ/EvBf7rOfk=", + "cpu": [ + "arm" + ], "dev": true, - "license": "MIT", + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">= 6" + "node": ">=10" } }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "node_modules/@swc/core-linux-arm64-gnu": { + "version": "1.15.43", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.15.43.tgz", + "integrity": "sha1-HrLZxe7uW7nQBZm0dd3DHcKHDSI=", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT" + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } }, - "node_modules/confbox": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", - "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", + "node_modules/@swc/core-linux-arm64-musl": { + "version": "1.15.43", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.15.43.tgz", + "integrity": "sha1-6mtcOAiPOSGleSLTkxstdP0jqf0=", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT" + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } }, - "node_modules/consola": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", - "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", + "node_modules/@swc/core-linux-ppc64-gnu": { + "version": "1.15.43", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@swc/core-linux-ppc64-gnu/-/core-linux-ppc64-gnu-1.15.43.tgz", + "integrity": "sha1-U4+sMLvV8eZ4u3usnMxiJGpvbXo=", + "cpu": [ + "ppc64" + ], "dev": true, - "license": "MIT", + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": "^14.18.0 || >=16.10.0" + "node": ">=10" } }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "node_modules/@swc/core-linux-s390x-gnu": { + "version": "1.15.43", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@swc/core-linux-s390x-gnu/-/core-linux-s390x-gnu-1.15.43.tgz", + "integrity": "sha1-7lZLRfP1eLH8ghNsTasWMYkxZkE=", + "cpu": [ + "s390x" + ], "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">= 8" + "node": ">=10" } }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, + "node_modules/@swc/core-linux-x64-gnu": { + "version": "1.15.33", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.15.33.tgz", + "integrity": "sha1-SfNlWO3gcucZmao38SM2fa7SpmI=", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } + "node": ">=10" } }, - "node_modules/deep-eql": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", - "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "node_modules/@swc/core-linux-x64-musl": { + "version": "1.15.43", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.15.43.tgz", + "integrity": "sha1-U59vJyHAzDLl21zw1FPIIEX2Zi0=", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT", + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=6" + "node": ">=10" } }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "node_modules/@swc/core-win32-arm64-msvc": { + "version": "1.15.43", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.15.43.tgz", + "integrity": "sha1-t7trYR1ISsGdDuIUaecBLWRsKLU=", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT" + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } }, - "node_modules/es-module-lexer": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", - "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "node_modules/@swc/core-win32-ia32-msvc": { + "version": "1.15.43", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.15.43.tgz", + "integrity": "sha1-5bJXIqfSe7DJqb3ueGPynIZ0Nk4=", + "cpu": [ + "ia32" + ], "dev": true, - "license": "MIT" + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } }, - "node_modules/esbuild": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", - "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "node_modules/@swc/core-win32-x64-msvc": { + "version": "1.15.43", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.15.43.tgz", + "integrity": "sha1-0ohCYhIBw0U4PUaNQMCWSLbNbmg=", + "cpu": [ + "x64" + ], "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.7", - "@esbuild/android-arm": "0.27.7", - "@esbuild/android-arm64": "0.27.7", - "@esbuild/android-x64": "0.27.7", - "@esbuild/darwin-arm64": "0.27.7", - "@esbuild/darwin-x64": "0.27.7", - "@esbuild/freebsd-arm64": "0.27.7", - "@esbuild/freebsd-x64": "0.27.7", - "@esbuild/linux-arm": "0.27.7", - "@esbuild/linux-arm64": "0.27.7", - "@esbuild/linux-ia32": "0.27.7", - "@esbuild/linux-loong64": "0.27.7", - "@esbuild/linux-mips64el": "0.27.7", - "@esbuild/linux-ppc64": "0.27.7", - "@esbuild/linux-riscv64": "0.27.7", - "@esbuild/linux-s390x": "0.27.7", - "@esbuild/linux-x64": "0.27.7", - "@esbuild/netbsd-arm64": "0.27.7", - "@esbuild/netbsd-x64": "0.27.7", - "@esbuild/openbsd-arm64": "0.27.7", - "@esbuild/openbsd-x64": "0.27.7", - "@esbuild/openharmony-arm64": "0.27.7", - "@esbuild/sunos-x64": "0.27.7", - "@esbuild/win32-arm64": "0.27.7", - "@esbuild/win32-ia32": "0.27.7", - "@esbuild/win32-x64": "0.27.7" + "node": ">=10" } }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "node_modules/@swc/core/node_modules/@swc/core-linux-x64-gnu": { + "version": "1.15.43", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.15.43.tgz", + "integrity": "sha1-5uO/6naSHH9eFtUKEmYV8uBM4cg=", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT", + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/eslint": { - "version": "9.39.4", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", - "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", + "node_modules/@swc/counter": { + "version": "0.1.3", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha1-zHRjvQKUlhHGMpWW/M0rDseCsOk=", "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.8.0", - "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.21.2", - "@eslint/config-helpers": "^0.4.2", - "@eslint/core": "^0.17.0", - "@eslint/eslintrc": "^3.3.5", - "@eslint/js": "9.39.4", - "@eslint/plugin-kit": "^0.4.1", - "@humanfs/node": "^0.16.6", - "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.4.2", - "@types/estree": "^1.0.6", - "ajv": "^6.14.0", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.6", - "debug": "^4.3.2", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.4.0", - "eslint-visitor-keys": "^4.2.1", - "espree": "^10.4.0", - "esquery": "^1.5.0", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^8.0.0", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.5", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://eslint.org/donate" - }, - "peerDependencies": { - "jiti": "*" - }, - "peerDependenciesMeta": { - "jiti": { - "optional": true - } + "license": "Apache-2.0" + }, + "node_modules/@swc/types": { + "version": "0.1.27", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@swc/types/-/types-0.1.27.tgz", + "integrity": "sha1-EggLDEJt6kUGNPIC2aPIKsOW55M=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3" } }, - "node_modules/eslint-plugin-unused-imports": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-unused-imports/-/eslint-plugin-unused-imports-4.4.1.tgz", - "integrity": "sha512-oZGYUz1X3sRMGUB+0cZyK2VcvRX5lm/vB56PgNNcU+7ficUCKm66oZWKUubXWnOuPjQ8PvmXtCViXBMONPe7tQ==", + "node_modules/@tybys/wasm-util": { + "version": "0.10.3", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", + "integrity": "sha1-AVy6np3UfOFNA9KoxdVHv7FpZl0=", "dev": true, "license": "MIT", - "peerDependencies": { - "@typescript-eslint/eslint-plugin": "^8.0.0-0 || ^7.0.0 || ^6.0.0 || ^5.0.0", - "eslint": "^10.0.0 || ^9.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "@typescript-eslint/eslint-plugin": { - "optional": true - } + "optional": true, + "dependencies": { + "tslib": "^2.4.0" } }, - "node_modules/eslint-scope": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", - "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "node_modules/@tybys/wasm-util/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha1-YS7+TtI11Wfoq6Xypfq3AoCt6D8=", "dev": true, - "license": "BSD-2-Clause", + "license": "0BSD", + "optional": true + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha1-jpzZ4cNYH6azQaWu1ViOsoW+C0o=", + "dev": true, + "license": "MIT", "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" } }, - "node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" + "node_modules/@types/debug": { + "version": "4.1.13", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@types/debug/-/debug-4.1.13.tgz", + "integrity": "sha1-ItHMnVQtNZPK6nZPl0MGqzYobuc=", + "license": "MIT", + "dependencies": { + "@types/ms": "*" } }, - "node_modules/eslint/node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha1-M0MRlx06BxIefrkbaEpgXn7qnL0=", "dev": true, "license": "MIT" }, - "node_modules/eslint/node_modules/brace-expansion": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", - "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha1-zz8Oh2177hWpOrkluCv1cKOQSiQ=", + "license": "MIT" + }, + "node_modules/@types/estree-jsx": { + "version": "1.0.5", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@types/estree-jsx/-/estree-jsx-1.0.5.tgz", + "integrity": "sha1-hYqI6iDzT+ZREfAFpon6Hr9w3Bg=", + "license": "MIT", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/@types/geojson": { + "version": "7946.0.16", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@types/geojson/-/geojson-7946.0.16.tgz", + "integrity": "sha1-jr5T1p762nBERU4zBcGQF9l87So=", + "license": "MIT" + }, + "node_modules/@types/hast": { + "version": "3.0.5", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@types/hast/-/hast-3.0.5.tgz", + "integrity": "sha1-SAIN5MDmNJL0yp20IGjBCPaLf48=", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha1-WWoXRyM2lNUPatinhp/Lb1bPWEE=", "dev": true, + "license": "MIT" + }, + "node_modules/@types/katex": { + "version": "0.16.8", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@types/katex/-/katex-0.16.8.tgz", + "integrity": "sha1-gL8+CBTQmoRkEqCw8UCUa3nDbD4=", + "license": "MIT" + }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha1-fM9y7dLxqn3TQ34YDGQ3NYWATdY=", "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "@types/unist": "*" } }, - "node_modules/eslint/node_modules/eslint-visitor-keys": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha1-BSqmekjszEMJ1/AZG35BQ0uQu3g=", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.19.43", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@types/node/-/node-20.19.43.tgz", + "integrity": "sha1-/Oz1gLpCoNtVz0BMNyyXlzw3bJc=", "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" } }, - "node_modules/eslint/node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "node_modules/@types/prismjs": { + "version": "1.26.6", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@types/prismjs/-/prismjs-1.26.6.tgz", + "integrity": "sha1-bqJ8Em1kUxmuT3BV7aY6noNcAYc=", + "license": "MIT" + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha1-5uWobWAr6spxzlFj+t9fldcJMcc=", + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.31", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@types/react/-/react-18.3.31.tgz", + "integrity": "sha1-teleKP/M6rjZgvM/LrB24XZTwqQ=", + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha1-uJ3fLNg7T+r8xOLqQa/fuVoNGU8=", "dev": true, "license": "MIT", - "engines": { - "node": ">= 4" + "peerDependencies": { + "@types/react": "^18.0.0" } }, - "node_modules/eslint/node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "node_modules/@types/react-syntax-highlighter": { + "version": "15.5.13", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@types/react-syntax-highlighter/-/react-syntax-highlighter-15.5.13.tgz", + "integrity": "sha1-xbr2KjIZs78o05z+pV0KSaJj0fI=", "dev": true, - "license": "ISC", + "license": "MIT", "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" + "@types/react": "*" } }, - "node_modules/espree": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", - "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha1-rKqw+RnOaczmKcLU7S60rcG2wgw=", + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.64.0", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.64.0.tgz", + "integrity": "sha1-caDD1fil5sXf208PBL0b+1ctXiQ=", "dev": true, - "license": "BSD-2-Clause", + "license": "MIT", "dependencies": { - "acorn": "^8.15.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.1" + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.64.0", + "@typescript-eslint/type-utils": "8.64.0", + "@typescript-eslint/utils": "8.64.0", + "@typescript-eslint/visitor-keys": "8.64.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://opencollective.com/eslint" + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.64.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/espree/node_modules/eslint-visitor-keys": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "node_modules/@typescript-eslint/parser": { + "version": "8.64.0", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@typescript-eslint/parser/-/parser-8.64.0.tgz", + "integrity": "sha1-yYZKHMKKE/8ppzFPve8FKLsSL3I=", "dev": true, - "license": "Apache-2.0", + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.64.0", + "@typescript-eslint/types": "8.64.0", + "@typescript-eslint/typescript-estree": "8.64.0", + "@typescript-eslint/visitor-keys": "8.64.0", + "debug": "^4.4.3" + }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://opencollective.com/eslint" + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/esquery": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", - "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "node_modules/@typescript-eslint/project-service": { + "version": "8.64.0", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@typescript-eslint/project-service/-/project-service-8.64.0.tgz", + "integrity": "sha1-FMTik5DXMlp/ihIYwniP1km4XaY=", "dev": true, - "license": "BSD-3-Clause", + "license": "MIT", "dependencies": { - "estraverse": "^5.1.0" + "@typescript-eslint/tsconfig-utils": "^8.64.0", + "@typescript-eslint/types": "^8.64.0", + "debug": "^4.4.3" }, "engines": { - "node": ">=0.10" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.64.0", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@typescript-eslint/scope-manager/-/scope-manager-8.64.0.tgz", + "integrity": "sha1-1F8VMEqUyFw52zF7cXsVj7YlmVg=", "dev": true, - "license": "BSD-2-Clause", + "license": "MIT", "dependencies": { - "estraverse": "^5.2.0" + "@typescript-eslint/types": "8.64.0", + "@typescript-eslint/visitor-keys": "8.64.0" }, "engines": { - "node": ">=4.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.64.0", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.64.0.tgz", + "integrity": "sha1-xirI6pFzw8rIs4uOZuMKBGtUiFE=", "dev": true, - "license": "BSD-2-Clause", + "license": "MIT", "engines": { - "node": ">=4.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/estree-walker": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", - "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "node_modules/@typescript-eslint/type-utils": { + "version": "8.64.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@typescript-eslint/type-utils/-/type-utils-8.64.0.tgz", + "integrity": "sha1-EG+n1Yz5z3dY892OQmrII37OrPM=", "dev": true, "license": "MIT", "dependencies": { - "@types/estree": "^1.0.0" + "@typescript-eslint/types": "8.64.0", + "@typescript-eslint/typescript-estree": "8.64.0", + "@typescript-eslint/utils": "8.64.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "node_modules/@typescript-eslint/types": { + "version": "8.64.0", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@typescript-eslint/types/-/types-8.64.0.tgz", + "integrity": "sha1-tB+O9d1AYWkIZYuZEZep1IbNpgs=", "dev": true, - "license": "BSD-2-Clause", + "license": "MIT", "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/expect-type": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", - "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=12.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.64.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@typescript-eslint/typescript-estree/-/typescript-estree-8.64.0.tgz", + "integrity": "sha1-uNUSVeLXJutL2A05ek+0FwwC7sw=", "dev": true, "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.64.0", + "@typescript-eslint/tsconfig-utils": "8.64.0", + "@typescript-eslint/types": "8.64.0", + "@typescript-eslint/visitor-keys": "8.64.0", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, "engines": { - "node": ">=12.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, - "peerDependencies": { - "picomatch": "^3 || ^4" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/file-entry-cache": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", - "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "node_modules/@typescript-eslint/utils": { + "version": "8.64.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@typescript-eslint/utils/-/utils-8.64.0.tgz", + "integrity": "sha1-mLsgEM+3VLQZhbnJPm6LPc171gA=", "dev": true, "license": "MIT", "dependencies": { - "flat-cache": "^4.0.0" + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.64.0", + "@typescript-eslint/types": "8.64.0", + "@typescript-eslint/typescript-estree": "8.64.0" }, "engines": { - "node": ">=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.64.0", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@typescript-eslint/visitor-keys/-/visitor-keys-8.64.0.tgz", + "integrity": "sha1-eghCHRDlSWBzM1LNfJX6sXhOhHM=", "dev": true, "license": "MIT", "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" + "@typescript-eslint/types": "8.64.0", + "eslint-visitor-keys": "^5.0.0" }, "engines": { - "node": ">=10" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/fix-dts-default-cjs-exports": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/fix-dts-default-cjs-exports/-/fix-dts-default-cjs-exports-1.0.1.tgz", - "integrity": "sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==", + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha1-njyUiWl4JNLUzjqK0SYo+R6fWb4=", "dev": true, - "license": "MIT", - "dependencies": { - "magic-string": "^0.30.17", - "mlly": "^1.7.4", - "rollup": "^4.34.8" + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, - "node_modules/flat-cache": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", - "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", - "dev": true, + "node_modules/@uiw/codemirror-extensions-basic-setup": { + "version": "4.25.11", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@uiw/codemirror-extensions-basic-setup/-/codemirror-extensions-basic-setup-4.25.11.tgz", + "integrity": "sha1-pF4GBOtqKf/6FStoTQx3Asbmm98=", "license": "MIT", "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.4" + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/commands": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/lint": "^6.0.0", + "@codemirror/search": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0" }, - "engines": { - "node": ">=16" + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + }, + "peerDependencies": { + "@codemirror/autocomplete": ">=6.0.0", + "@codemirror/commands": ">=6.0.0", + "@codemirror/language": ">=6.0.0", + "@codemirror/lint": ">=6.0.0", + "@codemirror/search": ">=6.0.0", + "@codemirror/state": ">=6.0.0", + "@codemirror/view": ">=6.0.0" } }, - "node_modules/flatted": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", - "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", - "dev": true, + "node_modules/@uiw/react-codemirror": { + "version": "4.25.11", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@uiw/react-codemirror/-/react-codemirror-4.25.11.tgz", + "integrity": "sha1-AfFUrM36PfZ03C4AQQZ/UU30j8I=", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.6", + "@codemirror/commands": "^6.1.0", + "@codemirror/state": "^6.1.1", + "@codemirror/theme-one-dark": "^6.0.0", + "@uiw/codemirror-extensions-basic-setup": "4.25.11", + "codemirror": "^6.0.0" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + }, + "peerDependencies": { + "@babel/runtime": ">=7.11.0", + "@codemirror/state": ">=6.0.0", + "@codemirror/theme-one-dark": ">=6.0.0", + "@codemirror/view": ">=6.0.0", + "codemirror": ">=6.0.0", + "react": ">=17.0.0", + "react-dom": ">=17.0.0" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.3", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@ungap/structured-clone/-/structured-clone-1.3.3.tgz", + "integrity": "sha1-CUBB4aTLGYfwODNUISgayL45C8w=", "license": "ISC" }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "node_modules/@vitejs/plugin-react": { + "version": "6.0.3", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@vitejs/plugin-react/-/plugin-react-6.0.3.tgz", + "integrity": "sha1-VfHX9VhTTRCu8DwAfcIIt8N3HOQ=", "dev": true, - "hasInstallScript": true, "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/glob": { - "version": "13.0.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", - "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", - "dev": true, - "license": "BlueOak-1.0.0", "dependencies": { - "minimatch": "^10.2.2", - "minipass": "^7.1.3", - "path-scurry": "^2.0.2" + "@rolldown/pluginutils": "^1.0.1" }, "engines": { - "node": "18 || 20 || >=22" + "node": "^20.19.0 || >=22.12.0" }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "peerDependencies": { + "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", + "babel-plugin-react-compiler": "^1.0.0", + "vite": "^8.0.0" + }, + "peerDependenciesMeta": { + "@rolldown/plugin-babel": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + } } }, - "node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "node_modules/@vitest/expect": { + "version": "4.1.10", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@vitest/expect/-/expect-4.1.10.tgz", + "integrity": "sha1-eZwG/ES7DPfieEE3tifFzBcyhdQ=", "dev": true, - "license": "ISC", + "license": "MIT", "dependencies": { - "is-glob": "^4.0.3" + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.10", + "@vitest/utils": "4.1.10", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" }, - "engines": { - "node": ">=10.13.0" + "funding": { + "url": "https://opencollective.com/vitest" } }, - "node_modules/globals": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "node_modules/@vitest/mocker": { + "version": "4.1.10", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@vitest/mocker/-/mocker-4.1.10.tgz", + "integrity": "sha1-JBOYerTNf6HCthS0BMQHv2rR6tE=", "dev": true, "license": "MIT", - "engines": { - "node": ">=18" + "dependencies": { + "@vitest/spy": "4.1.10", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } } }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "node_modules/@vitest/pretty-format": { + "version": "4.1.10", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@vitest/pretty-format/-/pretty-format-4.1.10.tgz", + "integrity": "sha1-dVQucnOgjMEP1Nja1OPrHxbNlYw=", "dev": true, "license": "MIT", - "engines": { - "node": ">=8" + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" } }, - "node_modules/ignore": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", - "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "node_modules/@vitest/runner": { + "version": "4.1.10", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@vitest/runner/-/runner-4.1.10.tgz", + "integrity": "sha1-/r8KIakWhCFCLRlVNw5gb+q2A1U=", "dev": true, "license": "MIT", - "engines": { - "node": ">= 4" + "dependencies": { + "@vitest/utils": "4.1.10", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" } }, - "node_modules/import-fresh": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", - "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "node_modules/@vitest/snapshot": { + "version": "4.1.10", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@vitest/snapshot/-/snapshot-4.1.10.tgz", + "integrity": "sha1-fj6f7H1NRyMuSTz9y9IXDeQ3HAQ=", "dev": true, "license": "MIT", "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" + "@vitest/pretty-format": "4.1.10", + "@vitest/utils": "4.1.10", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://opencollective.com/vitest" } }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "node_modules/@vitest/spy": { + "version": "4.1.10", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@vitest/spy/-/spy-4.1.10.tgz", + "integrity": "sha1-XAv6l7Vrup43QDyXbbd2/2q1b2U=", "dev": true, "license": "MIT", - "engines": { - "node": ">=0.8.19" + "funding": { + "url": "https://opencollective.com/vitest" } }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "node_modules/@vitest/utils": { + "version": "4.1.10", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@vitest/utils/-/utils-4.1.10.tgz", + "integrity": "sha1-/8cQVfGL/Msf0FhjZevCgkiS5AM=", "dev": true, "license": "MIT", - "engines": { - "node": ">=0.10.0" + "dependencies": { + "@vitest/pretty-format": "4.1.10", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" } }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha1-u89LpQdUZ/PyEx6rPP/HPC9deJU=", "license": "MIT", "dependencies": { - "is-extglob": "^2.1.1" + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" }, "engines": { - "node": ">=0.10.0" + "node": ">= 0.6" } }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, - "license": "ISC" - }, - "node_modules/joycon": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", - "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==", + "node_modules/acorn": { + "version": "8.17.0", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/acorn/-/acorn-8.17.0.tgz", + "integrity": "sha1-F4WtuE+vjYrdEDabk4Jvwr0I8f4=", "dev": true, "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, "engines": { - "node": ">=10" + "node": ">=0.4.0" } }, - "node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha1-ftW7VZCLOy8bxVxq8WU7rafweTc=", "dev": true, "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha1-MEs2Nq3Yi6fZNnYN1Q7OAG3qlfk=", + "license": "MIT", "dependencies": { - "argparse": "^2.0.1" + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" }, - "bin": { - "js-yaml": "bin/js-yaml.js" + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/json-buffer": { + "node_modules/ajv-formats": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "dev": true, + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha1-PV3HYryhdnnDwup+kK1rdTIwlXg=", "license": "MIT", "dependencies": { - "json-buffer": "3.0.1" + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } } }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha1-YCFu6kZNhkWXzigyAAc4oFiWUME=", "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, "engines": { - "node": ">= 0.8.0" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, - "node_modules/lilconfig": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", - "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", - "dev": true, + "node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha1-wETV3MUhoHZBNHJZehrLHxA8QEE=", "license": "MIT", "engines": { - "node": ">=14" + "node": ">=12" }, "funding": { - "url": "https://github.com/sponsors/antonk52" + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha1-q8av7tzqUugJzcA3au0845Y10X8=", "dev": true, "license": "MIT" }, - "node_modules/load-tsconfig": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/load-tsconfig/-/load-tsconfig-0.2.5.tgz", - "integrity": "sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==", + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha1-JG9Q88p4oyQPbJl+ipvR6sSeSzg=", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha1-9kGhlrM1aQsQcL8AtudZP+wZC/c=", "dev": true, "license": "MIT", "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "node": ">=12" } }, - "node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, + "node_modules/bail": { + "version": "2.0.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/bail/-/bail-2.0.2.tgz", + "integrity": "sha1-0m9c2P5db4MqMVF7n3w1YEC6bV0=", "license": "MIT", - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/loupe": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", - "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/lru-cache": { - "version": "11.3.6", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.6.tgz", - "integrity": "sha512-Gf/KoL3C/MlI7Bt0PGI9I+TeTC/I6r/csU58N4BSNc4lppLBeKsOdFYkK+dX0ABDUMJNfCHTyPpzwwO21Awd3A==", + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha1-v7EGYv7tgZaixi58aOF3IMJ0F5o=", "dev": true, - "license": "BlueOak-1.0.0", + "license": "MIT", "engines": { - "node": "20 || >=22" + "node": "18 || 20 || >=22" } }, - "node_modules/magic-string": { - "version": "0.30.21", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", - "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", - "dev": true, + "node_modules/body-parser": { + "version": "2.3.0", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/body-parser/-/body-parser-2.3.0.tgz", + "integrity": "sha1-bYZi9NjDNgKLismqJCUbDKZLpDc=", "license": "MIT", "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.5" - } - }, - "node_modules/minimatch": { - "version": "10.2.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", - "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "brace-expansion": "^5.0.5" + "bytes": "^3.1.2", + "content-type": "^2.0.0", + "debug": "^4.4.3", + "http-errors": "^2.0.1", + "iconv-lite": "^0.7.2", + "on-finished": "^2.4.1", + "qs": "^6.15.2", + "raw-body": "^3.0.2", + "type-is": "^2.1.0" }, "engines": { - "node": "18 || 20 || >=22" + "node": ">=18" }, "funding": { - "url": "https://github.com/sponsors/isaacs" + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/minipass": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", - "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", - "dev": true, - "license": "BlueOak-1.0.0", + "node_modules/body-parser/node_modules/content-type": { + "version": "2.0.0", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha1-L7Pt5p3/oK94ynxM51iWgGOLVt8=", + "license": "MIT", "engines": { - "node": ">=16 || 14 >=14.17" + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/mlly": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.2.tgz", - "integrity": "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==", + "node_modules/brace-expansion": { + "version": "5.0.7", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/brace-expansion/-/brace-expansion-5.0.7.tgz", + "integrity": "sha1-Gw5GlltHna1lr3N7SgJ5CgVJgzc=", "dev": true, "license": "MIT", "dependencies": { - "acorn": "^8.16.0", - "pathe": "^2.0.3", - "pkg-types": "^1.3.1", - "ufo": "^1.6.3" + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" } }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/mz": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", - "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/braces/-/braces-3.0.3.tgz", + "integrity": "sha1-SQMy9AkZRSJy1VqEgK3AxEE1h4k=", "dev": true, "license": "MIT", "dependencies": { - "any-promise": "^1.0.0", - "object-assign": "^4.0.1", - "thenify-all": "^1.0.0" + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" } }, - "node_modules/nanoid": { - "version": "3.3.12", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", - "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "node_modules/bundle-require": { + "version": "5.1.0", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/bundle-require/-/bundle-require-5.1.0.tgz", + "integrity": "sha1-jbZvQZUNo9d68e8zIvTD4EAJ+u4=", "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" + "dependencies": { + "load-tsconfig": "^0.2.3" }, "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "peerDependencies": { + "esbuild": ">=0.18" } }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true, - "license": "MIT" - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha1-iwvuuYYFrfGxKPpDhkA8AJ4CIaU=", "license": "MIT", "engines": { - "node": ">=0.10.0" + "node": ">= 0.8" } }, - "node_modules/optionator": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", - "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/cac/-/cac-6.7.14.tgz", + "integrity": "sha1-gE4eb1Bu42PLDjzLsJytXdmHCVk=", "dev": true, "license": "MIT", - "dependencies": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.5" - }, "engines": { - "node": ">= 0.8.0" + "node": ">=8" } }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha1-S1QowiK+mF15w9gmV0edvgtZstY=", "license": "MIT", "dependencies": { - "yocto-queue": "^0.1.0" + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">= 0.4" } }, - "node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha1-I43pNdKippKSjFOMfM+pEGf9Bio=", "license": "MIT", "dependencies": { - "p-limit": "^3.0.2" + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" }, "engines": { - "node": ">=10" + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/package-json-from-dist": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", - "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", - "dev": true, - "license": "BlueOak-1.0.0" - }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha1-s2MKvYlDQy9Us/BRkjjjPNffL3M=", "dev": true, "license": "MIT", - "dependencies": { - "callsites": "^3.0.0" - }, "engines": { "node": ">=6" } }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha1-F6O/gjAuCHDW2kOgExGovAKj7PU=", "license": "MIT", - "engines": { - "node": ">=8" + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/chai/-/chai-6.2.2.tgz", + "integrity": "sha1-rkG1LJrKh3NFBTYnF/MlX6zaNg4=", "dev": true, "license": "MIT", "engines": { - "node": ">=8" + "node": ">=18" } }, - "node_modules/path-scurry": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", - "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha1-qsTit3NKdAhnrrFr8CqtVWoeegE=", "dev": true, - "license": "BlueOak-1.0.0", + "license": "MIT", "dependencies": { - "lru-cache": "^11.0.0", - "minipass": "^7.1.2" + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" }, "engines": { - "node": "18 || 20 || >=22" + "node": ">=10" }, "funding": { - "url": "https://github.com/sponsors/isaacs" + "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/pathe": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", - "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", - "dev": true, - "license": "MIT" - }, - "node_modules/pathval": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", - "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "node_modules/chalk/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha1-7dgDYornHATIWuegkG7a00tkiTc=", "dev": true, "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, "engines": { - "node": ">= 14.16" + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", - "dev": true, + "node_modules/character-entities": { + "version": "2.0.2", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha1-LQnC5yzZUjB2zLIRV9/2atQ/zCI=", "license": "MIT", - "engines": { - "node": ">=12" - }, "funding": { - "url": "https://github.com/sponsors/jonschlinkert" + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/pirates": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", - "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", - "dev": true, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha1-HxrblAyXGksiujndymthjcblays=", "license": "MIT", - "engines": { - "node": ">= 6" + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/pkg-types": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", - "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", - "dev": true, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha1-dryDqQc4kB17wiOp6TdZ/dVgEls=", "license": "MIT", - "dependencies": { - "confbox": "^0.1.8", - "mlly": "^1.7.4", - "pathe": "^2.0.1" + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/postcss": { - "version": "8.5.14", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz", - "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], + "node_modules/character-reference-invalid": { + "version": "2.0.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", + "integrity": "sha1-hcZrBB5DtHIQ+vQBJ4q/gIrEXLk=", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/chart.js": { + "version": "4.5.1", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/chart.js/-/chart.js-4.5.1.tgz", + "integrity": "sha1-Gd0amjhqP2OXaRZyIxy1/JwFLDU=", "license": "MIT", "dependencies": { - "nanoid": "^3.3.11", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" + "@kurkle/color": "^0.3.0" }, "engines": { - "node": "^10 || ^12 || >=14" + "pnpm": ">=8" } }, - "node_modules/postcss-load-config": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", - "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha1-e+N6TAPJruHs/oYqSiOyxwwgXTA=", "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], "license": "MIT", "dependencies": { - "lilconfig": "^3.1.1" + "readdirp": "^4.0.1" }, "engines": { - "node": ">= 18" + "node": ">= 14.16.0" }, - "peerDependencies": { - "jiti": ">=1.21.0", - "postcss": ">=8.0.9", - "tsx": "^4.8.1", - "yaml": "^2.4.2" + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/cliui": { + "version": "9.0.1", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/cliui/-/cliui-9.0.1.tgz", + "integrity": "sha1-b3iQ84b28feZU63B943sRvzC0pE=", + "license": "ISC", + "dependencies": { + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" }, - "peerDependenciesMeta": { - "jiti": { - "optional": true - }, - "postcss": { - "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true - } + "engines": { + "node": ">=20" } }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true, + "node_modules/codemirror": { + "version": "6.0.2", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/codemirror/-/codemirror-6.0.2.tgz", + "integrity": "sha1-TT/qGtYLZ1P5fKg18vSMaTaolG4=", "license": "MIT", - "engines": { - "node": ">= 0.8.0" + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/commands": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/lint": "^6.0.0", + "@codemirror/search": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0" } }, - "node_modules/prettier": { - "version": "3.8.3", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.3.tgz", - "integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==", + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha1-ctOmjVmMm9s68q0ehPIdiWq9TeM=", "dev": true, "license": "MIT", - "bin": { - "prettier": "bin/prettier.cjs" + "dependencies": { + "color-name": "~1.1.4" }, "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" + "node": ">=7.0.0" } }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha1-wqCah6y95pVD3m9j+jmVyCbFNqI=", "dev": true, + "license": "MIT" + }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha1-TonJRYrLYbyP7xn0UplzsjkoOe4=", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/commander": { + "version": "8.3.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/commander/-/commander-8.3.0.tgz", + "integrity": "sha1-SDfqGy2me5xhamevuw+v7lZ7ymY=", "license": "MIT", "engines": { - "node": ">=6" + "node": ">= 12" } }, - "node_modules/readdirp": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", - "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true, + "license": "MIT" + }, + "node_modules/confbox": { + "version": "0.1.8", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/confbox/-/confbox-0.1.8.tgz", + "integrity": "sha1-gg1z07PILZvZEGUsXU1Znvj/iwY=", + "dev": true, + "license": "MIT" + }, + "node_modules/consola": { + "version": "3.4.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/consola/-/consola-3.4.2.tgz", + "integrity": "sha1-WvEQFFOXu2ev2rdwE/3DTK5ZDqc=", "dev": true, "license": "MIT", "engines": { - "node": ">= 14.18.0" + "node": "^14.18.0 || >=16.10.0" + } + }, + "node_modules/content-disposition": { + "version": "1.1.0", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha1-89t4nHUtRVZMx+nh4LMXkNSjjhc=", + "license": "MIT", + "engines": { + "node": ">=18" }, "funding": { - "type": "individual", - "url": "https://paulmillr.com/funding/" + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha1-i3cxYmVtHRCGeEyPI6VM5tc9eRg=", "license": "MIT", "engines": { - "node": ">=4" + "node": ">= 0.6" } }, - "node_modules/rimraf": { - "version": "6.1.3", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.1.3.tgz", - "integrity": "sha512-LKg+Cr2ZF61fkcaK1UdkH2yEBBKnYjTyWzTJT6KNPcSPaiT7HSdhtMXQuN5wkTX0Xu72KQ1l8S42rlmexS2hSA==", + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha1-S1YPZJ/E6RjdCrdc9JYei8iC2Co=", "dev": true, - "license": "BlueOak-1.0.0", + "license": "MIT" + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha1-VWNpxHKiupEPKXmJG1JrNDYjftc=", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha1-V8f8PMKTrKuf7FTXPhVpDr5KF5M=", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/cors/-/cors-2.8.6.tgz", + "integrity": "sha1-/13Wm9leVHUDgg0pq6T4+vjf7JY=", + "license": "MIT", "dependencies": { - "glob": "^13.0.3", - "package-json-from-dist": "^1.0.1" - }, - "bin": { - "rimraf": "dist/esm/bin.mjs" + "object-assign": "^4", + "vary": "^1" }, "engines": { - "node": "20 || >=22" + "node": ">= 0.10" }, "funding": { - "url": "https://github.com/sponsors/isaacs" + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/rollup": { - "version": "4.60.3", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.3.tgz", - "integrity": "sha512-pAQK9HalE84QSm4Po3EmWIZPd3FnjkShVkiMlz1iligWYkWQ7wHYd1PF/T7QZ5TVSD6uSTon5gBVMSM4JfBV+A==", - "dev": true, + "node_modules/crelt": { + "version": "1.0.7", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/crelt/-/crelt-1.0.7.tgz", + "integrity": "sha1-O0QbLd+nMWHWoncKpM1nf4leryg=", + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha1-ilj+ePANzXDDcEUXWd+/rwPo7p8=", "license": "MIT", "dependencies": { - "@types/estree": "1.0.8" - }, - "bin": { - "rollup": "dist/bin/rollup" + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" }, "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.60.3", - "@rollup/rollup-android-arm64": "4.60.3", - "@rollup/rollup-darwin-arm64": "4.60.3", - "@rollup/rollup-darwin-x64": "4.60.3", - "@rollup/rollup-freebsd-arm64": "4.60.3", - "@rollup/rollup-freebsd-x64": "4.60.3", - "@rollup/rollup-linux-arm-gnueabihf": "4.60.3", - "@rollup/rollup-linux-arm-musleabihf": "4.60.3", - "@rollup/rollup-linux-arm64-gnu": "4.60.3", - "@rollup/rollup-linux-arm64-musl": "4.60.3", - "@rollup/rollup-linux-loong64-gnu": "4.60.3", - "@rollup/rollup-linux-loong64-musl": "4.60.3", - "@rollup/rollup-linux-ppc64-gnu": "4.60.3", - "@rollup/rollup-linux-ppc64-musl": "4.60.3", - "@rollup/rollup-linux-riscv64-gnu": "4.60.3", - "@rollup/rollup-linux-riscv64-musl": "4.60.3", - "@rollup/rollup-linux-s390x-gnu": "4.60.3", - "@rollup/rollup-linux-x64-gnu": "4.60.3", - "@rollup/rollup-linux-x64-musl": "4.60.3", - "@rollup/rollup-openbsd-x64": "4.60.3", - "@rollup/rollup-openharmony-arm64": "4.60.3", - "@rollup/rollup-win32-arm64-msvc": "4.60.3", - "@rollup/rollup-win32-ia32-msvc": "4.60.3", - "@rollup/rollup-win32-x64-gnu": "4.60.3", - "@rollup/rollup-win32-x64-msvc": "4.60.3", - "fsevents": "~2.3.2" + "node": ">= 8" } }, - "node_modules/rollup/node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha1-7EjA8+mT5QZIyG2lWeJhCZXPmJo=", "license": "MIT" }, - "node_modules/semver": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", - "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", - "dev": true, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha1-Ff7DOyN/l6xdfJhtx32ic6jtC7U=", "license": "ISC", - "bin": { - "semver": "bin/semver.js" + "dependencies": { + "internmap": "1 - 2" }, "engines": { - "node": ">=10" + "node": ">=12" } }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "license": "MIT", + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha1-OVsoM9+scVB/EqwvevI7+BneJOI=", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha1-mBaQOHM6ClurvtpVBU95W7nkpYs=", + "license": "ISC", "dependencies": { - "shebang-regex": "^3.0.0" + "delaunator": "5" }, "engines": { - "node": ">=8" + "node": ">=12" } }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "license": "MIT", + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha1-X8dShOnCN1w2yDlBGgz1UMv8TV4=", + "license": "ISC", "engines": { - "node": ">=8" + "node": ">=12" } }, - "node_modules/siginfo": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", - "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", - "dev": true, - "license": "ISC" + "node_modules/d3-dsv": { + "version": "3.0.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/d3-dsv/-/d3-dsv-3.0.1.tgz", + "integrity": "sha1-xjr5ePTWoNCEpSpnOSK+IWB4m3M=", + "license": "ISC", + "dependencies": { + "commander": "7", + "iconv-lite": "0.6", + "rw": "1" + }, + "bin": { + "csv2json": "bin/dsv2json.js", + "csv2tsv": "bin/dsv2dsv.js", + "dsv2dsv": "bin/dsv2dsv.js", + "dsv2json": "bin/dsv2json.js", + "json2csv": "bin/json2dsv.js", + "json2dsv": "bin/json2dsv.js", + "json2tsv": "bin/json2dsv.js", + "tsv2csv": "bin/dsv2dsv.js", + "tsv2json": "bin/dsv2json.js" + }, + "engines": { + "node": ">=12" + } }, - "node_modules/source-map": { - "version": "0.7.6", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", - "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", - "dev": true, - "license": "BSD-3-Clause", + "node_modules/d3-dsv/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/commander/-/commander-7.2.0.tgz", + "integrity": "sha1-o2y1fQtQHOEI5NIFWaFQo5HZerc=", + "license": "MIT", "engines": { - "node": ">= 12" + "node": ">= 10" } }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, - "license": "BSD-3-Clause", + "node_modules/d3-dsv/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha1-pS+AvzjaGVLrXGgXkHGYcaGnJQE=", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, "engines": { "node": ">=0.10.0" } }, - "node_modules/stackback": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", - "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", - "dev": true, - "license": "MIT" + "node_modules/d3-force": { + "version": "3.0.0", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/d3-force/-/d3-force-3.0.0.tgz", + "integrity": "sha1-Piuhph5wiI/j2RlOMNbRTuzhVcQ=", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-quadtree": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + } }, - "node_modules/std-env": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", - "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", - "dev": true, - "license": "MIT" + "node_modules/d3-format": { + "version": "3.1.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/d3-format/-/d3-format-3.1.2.tgz", + "integrity": "sha1-Af20a1i+sfVbELQq1wtuNE1esq4=", + "license": "ISC", + "engines": { + "node": ">=12" + } }, - "node_modules/strip-json-comments": { + "node_modules/d3-geo": { "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/d3-geo/-/d3-geo-3.1.1.tgz", + "integrity": "sha1-YCfPUSRvmy69ZPmeAdx8M2QDOk0=", + "license": "ISC", + "dependencies": { + "d3-array": "2.5.0 - 3" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "engines": { + "node": ">=12" } }, - "node_modules/sucrase": { - "version": "3.35.1", - "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", - "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", - "dev": true, - "license": "MIT", + "node_modules/d3-geo-projection": { + "version": "4.0.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/d3-geo-projection/-/d3-geo-projection-4.0.0.tgz", + "integrity": "sha1-3CKeXq140xhppOh88fRb0nFsSMo=", + "license": "ISC", "dependencies": { - "@jridgewell/gen-mapping": "^0.3.2", - "commander": "^4.0.0", - "lines-and-columns": "^1.1.6", - "mz": "^2.7.0", - "pirates": "^4.0.1", - "tinyglobby": "^0.2.11", - "ts-interface-checker": "^0.1.9" + "commander": "7", + "d3-array": "1 - 3", + "d3-geo": "1.12.0 - 3" }, "bin": { - "sucrase": "bin/sucrase", - "sucrase-node": "bin/sucrase-node" + "geo2svg": "bin/geo2svg.js", + "geograticule": "bin/geograticule.js", + "geoproject": "bin/geoproject.js", + "geoquantize": "bin/geoquantize.js", + "geostitch": "bin/geostitch.js" }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": ">=12" } }, - "node_modules/supports-color": { + "node_modules/d3-geo-projection/node_modules/commander": { "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/commander/-/commander-7.2.0.tgz", + "integrity": "sha1-o2y1fQtQHOEI5NIFWaFQo5HZerc=", "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, "engines": { - "node": ">=8" + "node": ">= 10" } }, - "node_modules/thenify": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", - "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", - "dev": true, - "license": "MIT", - "dependencies": { - "any-promise": "^1.0.0" + "node_modules/d3-hierarchy": { + "version": "3.1.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", + "integrity": "sha1-sBzULB7tPUbbd6WWbPcm+MCRYMY=", + "license": "ISC", + "engines": { + "node": ">=12" } }, - "node_modules/thenify-all": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", - "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", - "dev": true, - "license": "MIT", + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha1-PEeqWzLFs9+1bvP9Q0IHimMrQA0=", + "license": "ISC", "dependencies": { - "thenify": ">= 3.1.0 < 4" + "d3-color": "1 - 3" }, "engines": { - "node": ">=0.8" + "node": ">=12" } }, - "node_modules/tinybench": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", - "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", - "dev": true, - "license": "MIT" + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha1-It+TkDL7WnGuixgA1h3beFHEJSY=", + "license": "ISC", + "engines": { + "node": ">=12" + } }, - "node_modules/tinyexec": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", - "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", - "dev": true, - "license": "MIT" + "node_modules/d3-quadtree": { + "version": "3.0.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/d3-quadtree/-/d3-quadtree-3.0.1.tgz", + "integrity": "sha1-bco+i+Kzk8mp1RTau9gKkt7vGk8=", + "license": "ISC", + "engines": { + "node": ">=12" + } }, - "node_modules/tinyglobby": { - "version": "0.2.16", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", - "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", - "dev": true, - "license": "MIT", + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha1-grOOjo/3CAdk+Nzsd71L45Nok5Y=", + "license": "ISC", "dependencies": { - "fdir": "^6.5.0", - "picomatch": "^4.0.4" + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" }, "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" + "node": ">=12" } }, - "node_modules/tinypool": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", - "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", - "dev": true, - "license": "MIT", + "node_modules/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha1-NMOdopiyPCDgLxpLI5vQ8i5/ExQ=", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-interpolate": "1 - 3" + }, "engines": { - "node": "^18.0.0 || >=20.0.0" + "node": ">=12" } }, - "node_modules/tinyrainbow": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", - "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", - "dev": true, - "license": "MIT", + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha1-oag5y9m6RfKGdMadf4Vbz5HfxqU=", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, "engines": { - "node": ">=14.0.0" + "node": ">=12" } }, - "node_modules/tinyspy": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", - "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", - "dev": true, - "license": "MIT", + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha1-kxDbVumS48AXXh7zheVF5Iqbtcc=", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, "engines": { - "node": ">=14.0.0" + "node": ">=12" } }, - "node_modules/tree-kill": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", - "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", - "dev": true, - "license": "MIT", - "bin": { - "tree-kill": "cli.js" + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha1-erUlelBB0R7LT+cKXH0WoZW7QIo=", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" } }, - "node_modules/ts-api-utils": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", - "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", - "dev": true, - "license": "MIT", + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha1-YoTSonCChbGrt+IB7aQ4CvNeY7A=", + "license": "ISC", "engines": { - "node": ">=18.12" - }, - "peerDependencies": { - "typescript": ">=4.8.4" + "node": ">=12" } }, - "node_modules/ts-interface-checker": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", - "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/tsup": { - "version": "8.5.1", - "resolved": "https://registry.npmjs.org/tsup/-/tsup-8.5.1.tgz", - "integrity": "sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing==", - "dev": true, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/debug/-/debug-4.4.3.tgz", + "integrity": "sha1-xq5DLZvZZiWC/OCHCbA4xY6ePWo=", "license": "MIT", "dependencies": { - "bundle-require": "^5.1.0", - "cac": "^6.7.14", - "chokidar": "^4.0.3", - "consola": "^3.4.0", - "debug": "^4.4.0", - "esbuild": "^0.27.0", - "fix-dts-default-cjs-exports": "^1.0.0", - "joycon": "^3.1.1", - "picocolors": "^1.1.1", - "postcss-load-config": "^6.0.1", - "resolve-from": "^5.0.0", - "rollup": "^4.34.8", - "source-map": "^0.7.6", - "sucrase": "^3.35.0", - "tinyexec": "^0.3.2", - "tinyglobby": "^0.2.11", - "tree-kill": "^1.2.2" - }, - "bin": { - "tsup": "dist/cli-default.js", - "tsup-node": "dist/cli-node.js" + "ms": "^2.1.3" }, "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@microsoft/api-extractor": "^7.36.0", - "@swc/core": "^1", - "postcss": "^8.4.12", - "typescript": ">=4.5.0" + "node": ">=6.0" }, "peerDependenciesMeta": { - "@microsoft/api-extractor": { - "optional": true - }, - "@swc/core": { - "optional": true - }, - "postcss": { - "optional": true - }, - "typescript": { + "supports-color": { "optional": true } } }, - "node_modules/tsup/node_modules/resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "node_modules/decode-named-character-reference": { + "version": "1.3.0", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz", + "integrity": "sha1-PkBgN2CHTC5YZ2kbWZ1zp9oltT8=", + "license": "MIT", + "dependencies": { + "character-entities": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha1-pvLc5hL63S7x9Rm3NVHxfoUZmDE=", "dev": true, + "license": "MIT" + }, + "node_modules/delaunator": { + "version": "5.1.0", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/delaunator/-/delaunator-5.1.0.tgz", + "integrity": "sha1-0TJx+/Ov9nU/nqbiNVV/IJAQRuo=", + "license": "ISC", + "dependencies": { + "robust-predicates": "^3.0.2" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/depd/-/depd-2.0.0.tgz", + "integrity": "sha1-tpYWPMdXVg0JzyLMj60Vcbeedt8=", "license": "MIT", "engines": { - "node": ">=8" + "node": ">= 0.8" } }, - "node_modules/type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha1-JkQhTxmX057Q7g7OcjNUkKesZ74=", "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1" - }, "engines": { - "node": ">= 0.8.0" + "node": ">=6" } }, - "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha1-aJxdzcGQDvVYOky59te0c3QgdK0=", "dev": true, "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, "engines": { - "node": ">=14.17" + "node": ">=8" } }, - "node_modules/typescript-eslint": { - "version": "8.59.3", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.59.3.tgz", - "integrity": "sha512-KgusgyDgG4LI8Ih/sWaCtZ06tckLAS5CvT5A4D1Q7bYVoAAyzwiZvE4BmwDHkhRVkvhRBepKeASoFzQetha7Fg==", - "dev": true, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha1-TbfCyk3G4Og0wwvnDJS7yXbccBg=", "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.59.3", - "@typescript-eslint/parser": "8.59.3", - "@typescript-eslint/typescript-estree": "8.59.3", - "@typescript-eslint/utils": "8.59.3" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "dequal": "^2.0.0" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha1-165mfh3INIL4tw/Q9u78UNow9Yo=", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" + "engines": { + "node": ">= 0.4" } }, - "node_modules/ufo": { - "version": "1.6.4", - "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.4.tgz", - "integrity": "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==", - "dev": true, + "node_modules/echarts": { + "version": "6.1.0", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/echarts/-/echarts-6.1.0.tgz", + "integrity": "sha1-rg9oWQ9eu9co2QCQfCes3nxUVtE=", + "license": "Apache-2.0", + "dependencies": { + "tslib": "2.3.0", + "zrender": "6.1.0" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=", "license": "MIT" }, - "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true, + "node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha1-vz1uj3+P0ipl2XA0dbwBRzV6aw0=", "license": "MIT" }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha1-e46omAd9fkCdOsRUdOo46vCFelg=", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/entities": { + "version": "6.0.1", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/entities/-/entities-6.0.1.tgz", + "integrity": "sha1-wow0pDN5yn9h0HQTCy9fcCCjBpQ=", "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" } }, - "node_modules/vite": { - "version": "5.4.21", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", - "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha1-mD6y+aZyTpMD9hrd8BHHLgngsPo=", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha1-BfdaJdq5jk+x3NXhRywFRtUFfI8=", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "2.3.1", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/es-module-lexer/-/es-module-lexer-2.3.1.tgz", + "integrity": "sha1-W/LfBpmdu+XwBqX0ahH7n1t7ORs=", "dev": true, + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha1-otCzcyBXJN+lJdI7DD4bHKWCyZs=", "license": "MIT", "dependencies": { - "esbuild": "^0.21.3", - "postcss": "^8.4.43", - "rollup": "^4.20.0" + "es-errors": "^1.3.0" }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.28.1", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha1-70W0Y0ycnZeilq6kEUpfmED5VXg=", + "dev": true, + "hasInstallScript": true, + "license": "MIT", "bin": { - "vite": "bin/vite.js" + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha1-ARo/aYVroYnf+n3I/M6Z0qh5A+U=", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=", + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha1-RoMSa1ALYXYvLb66zhgG6L4xscg=", + "license": "MIT", "engines": { - "node": "^18.0.0 || >=20.0.0" + "node": ">=12" }, "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.5", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/eslint/-/eslint-9.39.5.tgz", + "integrity": "sha1-Kk48iw91MZbvrpQ8j/qocw/Go/o=", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.2", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.6", + "@eslint/js": "9.39.5", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" }, - "optionalDependencies": { - "fsevents": "~2.3.3" + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" }, "peerDependencies": { - "@types/node": "^18.0.0 || >=20.0.0", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "sass-embedded": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.4.0" + "jiti": "*" }, "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { + "jiti": { "optional": true } } }, - "node_modules/vite-node": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.9.tgz", - "integrity": "sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==", + "node_modules/eslint-plugin-unused-imports": { + "version": "4.4.1", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/eslint-plugin-unused-imports/-/eslint-plugin-unused-imports-4.4.1.tgz", + "integrity": "sha1-qDHwopN9djHrowy4cJGrfTpdoOE=", "dev": true, "license": "MIT", - "dependencies": { - "cac": "^6.7.14", - "debug": "^4.3.7", - "es-module-lexer": "^1.5.4", - "pathe": "^1.1.2", - "vite": "^5.0.0" + "peerDependencies": { + "@typescript-eslint/eslint-plugin": "^8.0.0-0 || ^7.0.0 || ^6.0.0 || ^5.0.0", + "eslint": "^10.0.0 || ^9.0.0 || ^8.0.0" }, - "bin": { - "vite-node": "vite-node.mjs" + "peerDependenciesMeta": { + "@typescript-eslint/eslint-plugin": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha1-iOZGogf61hQ2/6OetQUUcgBlXII=", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" }, "engines": { - "node": "^18.0.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://opencollective.com/vitest" + "url": "https://opencollective.com/eslint" } }, - "node_modules/vite-node/node_modules/pathe": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", - "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/vite/node_modules/@esbuild/aix-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", - "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", - "cpu": [ - "ppc64" - ], + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha1-DNcv6FUOPC6uFWqWpN3c0cisWAA=", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], + "license": "Apache-2.0", "engines": { - "node": ">=12" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, - "node_modules/vite/node_modules/@esbuild/android-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", - "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", - "cpu": [ - "arm" - ], + "node_modules/eslint/node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha1-B+mCx0YmFnqnoklcU4F4ktcTlJI=", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/vite/node_modules/@esbuild/android-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", - "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", - "cpu": [ - "arm64" - ], + "node_modules/eslint/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha1-6D46fj8wCzTLnYf2FfoMvzV2kO4=", + "dev": true, + "license": "MIT" + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.16", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/brace-expansion/-/brace-expansion-1.1.16.tgz", + "integrity": "sha1-cj06MMBVjCJavJ/Eeac+FOJsPC8=", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" } }, - "node_modules/vite/node_modules/@esbuild/android-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", - "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", - "cpu": [ - "x64" - ], + "node_modules/eslint/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha1-FLqDpdNz49MR5a/KKc9b+tllvzQ=", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "android" - ], "engines": { - "node": ">=12" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/vite/node_modules/@esbuild/darwin-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", - "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", - "cpu": [ - "arm64" - ], + "node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha1-TP6mD+fdCtjoFuHtAmwdUlG1EsE=", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], + "license": "Apache-2.0", "engines": { - "node": ">=12" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, - "node_modules/vite/node_modules/@esbuild/darwin-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", - "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", - "cpu": [ - "x64" - ], + "node_modules/eslint/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha1-PNQOcp82Q/2HywTlC/DrcivFlvU=", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], "engines": { - "node": ">=12" + "node": ">= 4" } }, - "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", - "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", - "cpu": [ - "arm64" - ], + "node_modules/eslint/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha1-afaofZUTq4u4/mO9sJecRI5oRmA=", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], + "license": "MIT" + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha1-WAyI+NVEXyvWqo88re+g3nn71p4=", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, "engines": { - "node": ">=12" + "node": "*" } }, - "node_modules/vite/node_modules/@esbuild/freebsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", - "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", - "cpu": [ - "x64" - ], + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/espree/-/espree-10.4.0.tgz", + "integrity": "sha1-1U9JSdRikAWh+haNk3w/8ffiqDc=", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, "engines": { - "node": ">=12" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, - "node_modules/vite/node_modules/@esbuild/linux-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", - "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", - "cpu": [ - "arm" - ], + "node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha1-TP6mD+fdCtjoFuHtAmwdUlG1EsE=", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "license": "Apache-2.0", "engines": { - "node": ">=12" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, - "node_modules/vite/node_modules/@esbuild/linux-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", - "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", - "cpu": [ - "arm64" - ], + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha1-CNBI8mHw3e21uulfRoCUY9nJSW0=", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, "engines": { - "node": ">=12" + "node": ">=0.10" } }, - "node_modules/vite/node_modules/@esbuild/linux-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", - "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", - "cpu": [ - "ia32" - ], + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha1-eteWTWeauyi+5yzsY3WLHF0smSE=", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, "engines": { - "node": ">=12" + "node": ">=4.0" } }, - "node_modules/vite/node_modules/@esbuild/linux-loong64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", - "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", - "cpu": [ - "loong64" - ], + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha1-LupSkHAvJquP5TcDcP+GyWXSESM=", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "license": "BSD-2-Clause", "engines": { - "node": ">=12" + "node": ">=4.0" } }, - "node_modules/vite/node_modules/@esbuild/linux-mips64el": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", - "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", - "cpu": [ - "mips64el" - ], - "dev": true, + "node_modules/estree-util-is-identifier-name": { + "version": "3.0.0", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz", + "integrity": "sha1-C170xP8TUIs03NAez6lF9h/OXb0=", "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/vite/node_modules/@esbuild/linux-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", - "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", - "cpu": [ - "ppc64" - ], + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha1-Z8PlSexAKkh7T8GT0ZU6UkdSNA0=", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" + "dependencies": { + "@types/estree": "^1.0.0" } }, - "node_modules/vite/node_modules/@esbuild/linux-riscv64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", - "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", - "cpu": [ - "riscv64" - ], + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha1-dNLrTeC42hKTcRkQ1Qd1ubcQ72Q=", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "license": "BSD-2-Clause", "engines": { - "node": ">=12" + "node": ">=0.10.0" } }, - "node_modules/vite/node_modules/@esbuild/linux-s390x": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", - "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", - "cpu": [ - "s390x" - ], - "dev": true, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/etag/-/etag-1.8.1.tgz", + "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=", "license": "MIT", - "optional": true, - "os": [ - "linux" - ], "engines": { - "node": ">=12" + "node": ">= 0.6" } }, - "node_modules/vite/node_modules/@esbuild/linux-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", - "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", - "cpu": [ - "x64" - ], - "dev": true, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha1-EVdiLi9Td7tq7yEUNycougwVaYk=", "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "eventsource-parser": "^3.0.1" + }, "engines": { - "node": ">=12" + "node": ">=18.0.0" } }, - "node_modules/vite/node_modules/@esbuild/netbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", - "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", - "cpu": [ - "x64" - ], - "dev": true, + "node_modules/eventsource-parser": { + "version": "3.1.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/eventsource-parser/-/eventsource-parser-3.1.0.tgz", + "integrity": "sha1-ThmOuRzTM9Co3cwDZQKzYYol9Ek=", "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], "engines": { - "node": ">=12" + "node": ">=18.0.0" } }, - "node_modules/vite/node_modules/@esbuild/openbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", - "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", - "cpu": [ - "x64" - ], + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha1-JO338MxppE0AhWe6RZSrlvPDo9Y=", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], + "license": "Apache-2.0", "engines": { - "node": ">=12" + "node": ">=12.0.0" } }, - "node_modules/vite/node_modules/@esbuild/sunos-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", - "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", - "cpu": [ - "x64" - ], - "dev": true, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/express/-/express-5.2.1.tgz", + "integrity": "sha1-jyHRW20yf5K0eU7PjLCKcvlWrAQ=", "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, "engines": { - "node": ">=12" + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/vite/node_modules/@esbuild/win32-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", - "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", - "cpu": [ - "arm64" - ], - "dev": true, + "node_modules/express-rate-limit": { + "version": "8.6.0", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/express-rate-limit/-/express-rate-limit-8.6.0.tgz", + "integrity": "sha1-LmSrEDYdo1iBUZ6pK0JqQ/iqjHk=", "license": "MIT", - "optional": true, - "os": [ - "win32" - ], + "dependencies": { + "debug": "^4.4.3", + "ip-address": "^10.2.0" + }, "engines": { - "node": ">=12" + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" } }, - "node_modules/vite/node_modules/@esbuild/win32-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", - "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", - "cpu": [ - "ia32" - ], + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/extend/-/extend-3.0.2.tgz", + "integrity": "sha1-+LETa0Bx+9jrFAr/hYsQGewpFfo=", + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha1-On1WtVnWy8PrUSMlJE5hmmXGxSU=", + "license": "MIT" + }, + "node_modules/fast-json-patch": { + "version": "3.1.1", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/fast-json-patch/-/fast-json-patch-3.1.1.tgz", + "integrity": "sha1-hQZOobHr+Xo/etAeI/kzfnLGaUc=", + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha1-h0v2nG9ATCtdmcSBNBOZ/VWJJjM=", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.3", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/fast-uri/-/fast-uri-3.1.3.tgz", + "integrity": "sha1-9pWkDwBqulBWMVc6ACHdshGUrRE=", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } ], - "engines": { - "node": ">=12" + "license": "BSD-3-Clause" + }, + "node_modules/fault": { + "version": "1.0.4", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/fault/-/fault-1.0.4.tgz", + "integrity": "sha1-6vz8Cm0hT8lGAeFw3ymVSk+ELxM=", + "license": "MIT", + "dependencies": { + "format": "^0.2.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/vite/node_modules/@esbuild/win32-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", - "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", - "cpu": [ - "x64" - ], + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha1-7Sq5Z6MxreYvGNB32uGSaE1Q01A=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha1-d4e93PETG/+5JjbGlFe7wO3W2B8=", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha1-RCZdPKwH4+p9wkdRY4BkN1SgUpI=", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha1-osUXplWYUrzbBtH4vX9Rto+tgJk=", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha1-TJKBnstwg1YeT0okCoa+UZj1Nvw=", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/fix-dts-default-cjs-exports": { + "version": "1.0.1", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/fix-dts-default-cjs-exports/-/fix-dts-default-cjs-exports-1.0.1.tgz", + "integrity": "sha1-lVy2s9UZaRxXgosHitrfLLkulUk=", + "dev": true, + "license": "MIT", + "dependencies": { + "magic-string": "^0.30.17", + "mlly": "^1.7.4", + "rollup": "^4.34.8" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha1-Ds45/LFO4BL0sEEL0z3ZwfAREnw=", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha1-9cI8EH8PN96NvfJPE3IrO5jVJyY=", + "dev": true, + "license": "ISC" + }, + "node_modules/flint-chart": { + "resolved": "packages/flint-js", + "link": true + }, + "node_modules/flint-chart-mcp": { + "resolved": "packages/flint-mcp", + "link": true + }, + "node_modules/flint-chart-site": { + "resolved": "site", + "link": true + }, + "node_modules/format": { + "version": "0.2.2", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/format/-/format-0.2.2.tgz", + "integrity": "sha1-1hcBB+nv3E7TDJ3DkBbflCtctYs=", + "engines": { + "node": ">=0.4.x" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha1-ImmTZCiq1MFcfr6XeahL8LKoGBE=", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha1-jdffahs6Gzpc8YbAWl3SZ2ImNaQ=", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha1-ysZAd4XQNnWipeGlMFxpezR9kNY=", "dev": true, "license": "MIT", "optional": true, "os": [ - "win32" + "darwin" ], "engines": { - "node": ">=12" + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha1-LALYZNl/PqbIgwxGTL0Rq26rehw=", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha1-T5RBKoLbMvNuOwuXQfipf+sDH34=", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.6.0", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", + "integrity": "sha1-IWkA+R3xGossGYw+HZPWwDWndrk=", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha1-dD8OO2lkqTpUke0b/6rgVNf5jQE=", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha1-FQs/J0OGnvPoUewMSdFbHRTQDuE=", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob": { + "version": "13.0.6", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/glob/-/glob-13.0.6.tgz", + "integrity": "sha1-B4ZmVmpCUUfMrPvS4zLetmor5x0=", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha1-bSN9mQg5UMeSkPJMdkKj3poo+eM=", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "14.0.0", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/globals/-/globals-14.0.0.tgz", + "integrity": "sha1-iY10E8Kbq89rr+Vvyt3thYrack4=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha1-ifVrghe9vIgCvSmd9tfxCB1+UaE=", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha1-lEdx/ZyByBJlxNaUGGDaBrtZR5s=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha1-/JxqeDoISVHQuXH+EBjegTcHozg=", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha1-jGLYy5C+sqrV0KW2dYGtmFTD8AM=", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hast-util-from-dom": { + "version": "5.0.1", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/hast-util-from-dom/-/hast-util-from-dom-5.0.1.tgz", + "integrity": "sha1-w8kvvY1OHBYl7es6dzlSueStZKg=", + "license": "ISC", + "dependencies": { + "@types/hast": "^3.0.0", + "hastscript": "^9.0.0", + "web-namespaces": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-from-html": { + "version": "2.0.3", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/hast-util-from-html/-/hast-util-from-html-2.0.3.tgz", + "integrity": "sha1-SFx0eFNYvrgMS6Y0YpkxGsTEnII=", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "devlop": "^1.1.0", + "hast-util-from-parse5": "^8.0.0", + "parse5": "^7.0.0", + "vfile": "^6.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-from-html-isomorphic": { + "version": "2.0.0", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/hast-util-from-html-isomorphic/-/hast-util-from-html-isomorphic-2.0.0.tgz", + "integrity": "sha1-sxuu44aomaJHIyajxWkvKfhtHTw=", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-from-dom": "^5.0.0", + "hast-util-from-html": "^2.0.0", + "unist-util-remove-position": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-from-parse5": { + "version": "8.0.3", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/hast-util-from-parse5/-/hast-util-from-parse5-8.0.3.tgz", + "integrity": "sha1-gwo1Ai//KMP+o2l6mML0zGuDWi4=", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "devlop": "^1.0.0", + "hastscript": "^9.0.0", + "property-information": "^7.0.0", + "vfile": "^6.0.0", + "vfile-location": "^5.0.0", + "web-namespaces": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-is-element": { + "version": "3.0.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/hast-util-is-element/-/hast-util-is-element-3.0.0.tgz", + "integrity": "sha1-bjGmUywhfltTOEjH5Sydk2nKCTI=", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-parse-selector": { + "version": "4.0.0", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz", + "integrity": "sha1-NSh5+obiVhYDYDfdiTH7XzTLSic=", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-jsx-runtime": { + "version": "2.3.6", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz", + "integrity": "sha1-/zGJeq5Z9iIy4hWU6sfva2MzPpg=", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-js": "^1.0.0", + "unist-util-position": "^5.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-text": { + "version": "4.0.2", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/hast-util-to-text/-/hast-util-to-text-4.0.2.tgz", + "integrity": "sha1-V7Z2kx5xv5y4UkU2eElbMIC/rj4=", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "hast-util-is-element": "^3.0.0", + "unist-util-find-after": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha1-d3jtnTyS3Z6MXI9kiknCH8UctiE=", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hastscript": { + "version": "9.0.1", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/hastscript/-/hastscript-9.0.1.tgz", + "integrity": "sha1-28hL72BR1ACENCwinEUc2dxWff8=", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-parse-selector": "^4.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/highlight.js": { + "version": "10.7.3", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/highlight.js/-/highlight.js-10.7.3.tgz", + "integrity": "sha1-aXJy45kTVuQMPKxWanTu9oF1ZTE=", + "license": "BSD-3-Clause", + "engines": { + "node": "*" + } + }, + "node_modules/highlightjs-vue": { + "version": "1.0.0", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/highlightjs-vue/-/highlightjs-vue-1.0.0.tgz", + "integrity": "sha1-/f6X++pjVOcO5E46lVh14RTbCG0=", + "license": "CC0-1.0" + }, + "node_modules/hono": { + "version": "4.12.30", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/hono/-/hono-4.12.30.tgz", + "integrity": "sha1-TVewUcZmF7ADt7oxiRDLW3nEx6w=", + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/html-parse-stringify": { + "version": "3.0.1", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", + "integrity": "sha1-38EBc0fOn3fIFBpQfyMwQMWcVdI=", + "license": "MIT", + "dependencies": { + "void-elements": "3.1.0" + } + }, + "node_modules/html-url-attributes": { + "version": "3.0.1", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/html-url-attributes/-/html-url-attributes-3.0.1.tgz", + "integrity": "sha1-g7BSzV5DcHG3Vs10rnD3CIcMLYc=", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha1-NtL2W8kJyHkAGN02+02T2myq4Gs=", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/i18next": { + "version": "26.3.6", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/i18next/-/i18next-26.3.6.tgz", + "integrity": "sha1-WZ2ydcULZtKKadj2xRYsJFzpKWs=", + "funding": [ + { + "type": "individual", + "url": "https://www.locize.com/i18next" + }, + { + "type": "individual", + "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" + }, + { + "type": "individual", + "url": "https://www.locize.com" + } + ], + "license": "MIT", + "peerDependencies": { + "typescript": "^5 || ^6 || ^7" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/iconv-lite": { + "version": "0.7.3", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/iconv-lite/-/iconv-lite-0.7.3.tgz", + "integrity": "sha1-hO4S+WPn3lC8AaE+FgoHizsPQV8=", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ignore": { + "version": "7.0.6", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/ignore/-/ignore-7.0.6.tgz", + "integrity": "sha1-aleq70yQ3yesNZCHXSno8RmIyI4=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha1-nOy1ZQPAraHydB271lRuSxO1fM8=", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-fresh/node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha1-SrzYUq0y3Xuqv+m0DgCjbbXzkuY=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha1-D6LGT5MpF8NDOg3tVTY6rjdBa3w=", + "license": "ISC" + }, + "node_modules/inline-style-parser": { + "version": "0.2.7", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/inline-style-parser/-/inline-style-parser-0.2.7.tgz", + "integrity": "sha1-sfxov8AxO4aFdF5EZON/k3a5yQk=", + "license": "MIT" + }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha1-ZoXyN1XkPFJOJR0py8lySOMGEAk=", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/ip-address": { + "version": "10.2.0", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/ip-address/-/ip-address-10.2.0.tgz", + "integrity": "sha1-gF/BeLIMUYvUyFSLJP4wiS1/MgY=", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha1-v/OFQ+64mEglB5/zoqjmy9RngbM=", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-alphabetical": { + "version": "2.0.1", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/is-alphabetical/-/is-alphabetical-2.0.1.tgz", + "integrity": "sha1-AQcgU+p8EDbfPH0Zptqux/GeeJs=", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-alphanumerical": { + "version": "2.0.1", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", + "integrity": "sha1-fAP76W4+kxET5X+WSwo2jMLf2HU=", + "license": "MIT", + "dependencies": { + "is-alphabetical": "^2.0.0", + "is-decimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-decimal": { + "version": "2.0.1", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/is-decimal/-/is-decimal-2.0.1.tgz", + "integrity": "sha1-lGnS3BkNAhT9h9eLeMrswMwU7vc=", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha1-ZPYeQsu7LuwgcanawLKLoeZdUIQ=", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-hexadecimal": { + "version": "2.0.1", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", + "integrity": "sha1-hrW/Zo/KMHSY0xnfwDKJ14GpACc=", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha1-dTU0W4lnNNX4DE0GxQlVUnoU8Ss=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha1-1lAl7ew2V84DL9fbY8l4g+rtcfA=", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha1-Qv+fhCBsGZHSbev1IN1cAQQt0vM=", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "license": "ISC" + }, + "node_modules/jose": { + "version": "6.2.3", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/jose/-/jose-6.2.3.tgz", + "integrity": "sha1-CXUZetlzJRIhxlijzdxLlRolDC0=", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/joycon": { + "version": "3.1.1", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/joycon/-/joycon-3.1.1.tgz", + "integrity": "sha1-vOhZbWroCPi2gWj1/GkoCZaJTwM=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha1-GSA/tZmR35jjoocFDUZHzerzJJk=", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.3.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/js-yaml/-/js-yaml-4.3.0.tgz", + "integrity": "sha1-0ZAFcqf3zwtfVAyDZz5gutNDZZI=", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha1-kziAKjDTtmBfvgYT4JQAjKjAWhM=", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha1-rnvLNlard6c7pcSb9lTzjmtoYOI=", + "license": "MIT" + }, + "node_modules/json-schema-typed": { + "version": "8.0.2", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/json-schema-typed/-/json-schema-typed-8.0.2.tgz", + "integrity": "sha1-6Y7nsYmf9KGEU00fFnwojGa77/Q=", + "license": "BSD-2-Clause" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stringify-pretty-compact": { + "version": "4.0.0", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/json-stringify-pretty-compact/-/json-stringify-pretty-compact-4.0.0.tgz", + "integrity": "sha1-z0hEdwvd7jy4mmFw/ksA7uXb8dQ=", + "license": "MIT" + }, + "node_modules/katex": { + "version": "0.17.0", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/katex/-/katex-0.17.0.tgz", + "integrity": "sha1-U24lh07JrIco47S3v3/sZVe32/Q=", + "funding": [ + "https://opencollective.com/katex", + "https://github.com/sponsors/katex" + ], + "license": "MIT", + "dependencies": { + "commander": "^8.3.0" + }, + "bin": { + "katex": "cli.js" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha1-qHmpnilFL5QkOfKkBeOvizHU3pM=", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/levn/-/levn-0.4.1.tgz", + "integrity": "sha1-rkViwAdHO5MqYgDUAyaN0v/8at4=", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha1-uFqulkhtyxv0mnyFcSISc/Tx5Kk=", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha1-8DOIURbf79nG9UeHUj41FLYeGWg=", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha1-ULcYcbAcgZlYS2SeKSVH+up6+bU=", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha1-NfPpczLRMLnKGB4RtWje1q68bV4=", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha1-l3enZHK2Ttb/lDQq1kx7r9eUpXU=", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha1-E65lLhq3O5E117faFy9mbEEK1T0=", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha1-QXhYeVqUWS9oASOhsfnaig4e8zU=", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha1-a+NmkugQtxgECAL9gJYjz/5zITM=", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha1-C3gDr06yHP043Tn+Kru1PH3QkfY=", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha1-iNyLqGXd3bGsXvBLDxYYBEGMFjs=", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha1-TzC6P6XpJfW3n5RejMDRdsOxqzg=", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha1-FBqlYFZFBkkokCu0rwRfp9n0Igo=", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha1-obz9Ylf5WFv1rhTO7rt7VZAl5MQ=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha1-7KKE910pZQeTCdwK2SVauy68FjI=", + "dev": true, + "license": "MIT" + }, + "node_modules/load-tsconfig": { + "version": "0.2.5", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/load-tsconfig/-/load-tsconfig-0.2.5.tgz", + "integrity": "sha1-RTuM2JYb+5Et6nfrbBaP6Myj06E=", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha1-VTIeswn+u8WcSAHZMackUqaB0oY=", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha1-VYqlO0O2YeGSWgr9+japoQhf5Xo=", + "dev": true, + "license": "MIT" + }, + "node_modules/longest-streak": { + "version": "3.1.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/longest-streak/-/longest-streak-3.1.0.tgz", + "integrity": "sha1-YvpnzZWHQqFXSvnzmGY2QQLZDNQ=", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha1-ce5R+nvkyuwaY4OffmgtgTLTDK8=", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lowlight": { + "version": "1.20.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/lowlight/-/lowlight-1.20.0.tgz", + "integrity": "sha1-3bGX0zRirQ2TvxnRe2wwGqOUGIg=", + "license": "MIT", + "dependencies": { + "fault": "^1.0.0", + "highlight.js": "~10.7.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/lru-cache": { + "version": "11.5.2", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/lru-cache/-/lru-cache-11.5.2.tgz", + "integrity": "sha1-AOFmZckMYg+6FKPDaHMql2ST92A=", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha1-VnY+wJoPqAkd8nh5/ZTRkHjADZE=", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/markdown-table": { + "version": "3.0.4", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/markdown-table/-/markdown-table-3.0.4.tgz", + "integrity": "sha1-/kTW1BD/nW8uoXl6P2CqTStjHCo=", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha1-oN10voHiqlwvJ+Zc4oNgXuTit/k=", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mdast-util-find-and-replace": { + "version": "3.0.2", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz", + "integrity": "sha1-cKMXTIlOFN9yKr9DvCUMuuRLEd8=", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "escape-string-regexp": "^5.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-from-markdown": { + "version": "2.0.3", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.3.tgz", + "integrity": "sha1-yVgiuRqrdfGKTL6LL1G4c+0s8Mc=", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm": { + "version": "3.1.0", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz", + "integrity": "sha1-LN9juSwqMxQGsPsNtMB3wbAzF1E=", + "license": "MIT", + "dependencies": { + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-gfm-autolink-literal": "^2.0.0", + "mdast-util-gfm-footnote": "^2.0.0", + "mdast-util-gfm-strikethrough": "^2.0.0", + "mdast-util-gfm-table": "^2.0.0", + "mdast-util-gfm-task-list-item": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-autolink-literal": { + "version": "2.0.1", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz", + "integrity": "sha1-q9VXYwM3vTCm1aS9glLhwtwIddU=", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "ccount": "^2.0.0", + "devlop": "^1.0.0", + "mdast-util-find-and-replace": "^3.0.0", + "micromark-util-character": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz", + "integrity": "sha1-d3jp2co99yOMwr0/orG/amWxlAM=", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-strikethrough": { + "version": "2.0.0", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz", + "integrity": "sha1-1E756O0oOsjBFlqw0N/QWMJ2TBY=", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-table": { + "version": "2.0.0", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz", + "integrity": "sha1-ekNftiI6crCGKzOvvXErba6HjTg=", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "markdown-table": "^3.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-task-list-item": { + "version": "2.0.0", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz", + "integrity": "sha1-5oCV0vikMD7yQJSrZC4QR7mRqTY=", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-math": { + "version": "3.0.0", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/mdast-util-math/-/mdast-util-math-3.0.0.tgz", + "integrity": "sha1-jXndO6+KuKx4H2K4hTdoGQuaALA=", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "longest-streak": "^3.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.1.0", + "unist-util-remove-position": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-expression": { + "version": "2.0.1", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz", + "integrity": "sha1-Q/CrrJrcdW4ghvY4IqOMjTw6UJY=", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-jsx": { + "version": "3.2.0", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz", + "integrity": "sha1-/QTGeip0me+5BailxXjd3J/a2g0=", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "parse-entities": "^4.0.0", + "stringify-entities": "^4.0.0", + "unist-util-stringify-position": "^4.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdxjs-esm": { + "version": "2.0.1", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz", + "integrity": "sha1-AZz751etYt1VfbNaaV5zFLzJ+pc=", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-phrasing": { + "version": "4.1.0", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", + "integrity": "sha1-fMCo3sMOrwS3salmGpKtszgqpuM=", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.1", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", + "integrity": "sha1-1/+EykmaV+LAYK5nVIrZUOaJoFM=", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-markdown": { + "version": "2.1.2", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz", + "integrity": "sha1-+RD/5giX8Eu0t+fuQ0SG92KINhs=", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-string": { + "version": "4.0.0", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha1-elEhR1VWoE5+3etnsmSq550xKBQ=", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha1-ardLjy0zIPIGSyqHo455Mf86VWE=", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha1-6pIvZgY1oiSe5WXgRJ+VHmtgOAg=", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/micromark": { + "version": "4.0.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha1-kTlaPhiEoZjmIRbjPJxWjjmTb9s=", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-core-commonmark": { + "version": "2.0.3", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", + "integrity": "sha1-xpFjDkhQIaaM8o28Kyyifr9njNQ=", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-destination": "^2.0.0", + "micromark-factory-label": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-title": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-html-tag-name": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-gfm": { + "version": "3.0.0", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz", + "integrity": "sha1-PhM3arld16XP0OKVYN/pmWV7PFs=", + "license": "MIT", + "dependencies": { + "micromark-extension-gfm-autolink-literal": "^2.0.0", + "micromark-extension-gfm-footnote": "^2.0.0", + "micromark-extension-gfm-strikethrough": "^2.0.0", + "micromark-extension-gfm-table": "^2.0.0", + "micromark-extension-gfm-tagfilter": "^2.0.0", + "micromark-extension-gfm-task-list-item": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-autolink-literal": { + "version": "2.1.0", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz", + "integrity": "sha1-Yoau6WhsRGLB41UqnVBf7dzuuTU=", + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz", + "integrity": "sha1-TatW1OOYuYU/b+TvrE/JNh8+B1A=", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-strikethrough": { + "version": "2.1.0", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz", + "integrity": "sha1-hhBt+LOmkrX2qSKA04eb5r5G2SM=", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-table": { + "version": "2.1.1", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz", + "integrity": "sha1-+scLy/Uf5l9fRAMxGNOb6Km1lAs=", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-tagfilter": { + "version": "2.0.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz", + "integrity": "sha1-8m2KeAe1mF+6E89hRltYyl/33Fc=", + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-task-list-item": { + "version": "2.1.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz", + "integrity": "sha1-vMNNgFY5gpmQ7BdcPuoSu1t4Hyw=", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-math": { + "version": "3.1.0", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/micromark-extension-math/-/micromark-extension-math-3.1.0.tgz", + "integrity": "sha1-xC7jsd1amgNYToPdjwjj3lECEsE=", + "license": "MIT", + "dependencies": { + "@types/katex": "^0.16.0", + "devlop": "^1.0.0", + "katex": "^0.16.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-math/node_modules/katex": { + "version": "0.16.47", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/katex/-/katex-0.16.47.tgz", + "integrity": "sha1-ChOkLC3rT3TmHxYtRAuRZaVIAw8=", + "funding": [ + "https://opencollective.com/katex", + "https://github.com/sponsors/katex" + ], + "license": "MIT", + "dependencies": { + "commander": "^8.3.0" + }, + "bin": { + "katex": "cli.js" + } + }, + "node_modules/micromark-factory-destination": { + "version": "2.0.1", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", + "integrity": "sha1-j++OD3CB8EdPvdkt61DJkKAmRjk=", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-label": { + "version": "2.0.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", + "integrity": "sha1-UmfvqX8eUlTvx/ILRZo4yyEFi6E=", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha1-NtAhLpYrKzEh+FJfx6PHwCnzNPw=", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-title": { + "version": "2.0.1", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", + "integrity": "sha1-I35KpdWKlYY/AQMtnumwkPHebpQ=", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-whitespace": { + "version": "2.0.1", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", + "integrity": "sha1-BrJrKYPE0nv8xlezPiUTTUhosLE=", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha1-L5h4MaQNTFEKwmHomFLE6XA8zaY=", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-chunked": { + "version": "2.0.1", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", + "integrity": "sha1-R/vNk0caP8yrhs/wOEf8NVLbEFE=", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-classify-character": { + "version": "2.0.1", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", + "integrity": "sha1-05n6+cRcoUyLS+mLHqSBvO2Htik=", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-combine-extensions": { + "version": "2.0.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", + "integrity": "sha1-Kg9JCrCL/1zC/V7sbdDKBPibMKk=", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-numeric-character-reference": { + "version": "2.0.2", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", + "integrity": "sha1-/PFbZgl5OI5vEYzba/fXnXPSb+U=", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-string": { + "version": "2.0.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", + "integrity": "sha1-bLmVguXScehO/KjmGoB5lNcWHrI=", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha1-DVHRwJVVHPqsNoMmljz1XxX1QLg=", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-html-tag-name": { + "version": "2.0.1", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", + "integrity": "sha1-5AQDCWSBmGtBwQZif5j3LU0QuCU=", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-normalize-identifier": { + "version": "2.0.1", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", + "integrity": "sha1-ww13sugyrPZSb4vxqke8nJQ4wW0=", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-resolve-all": { + "version": "2.0.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", + "integrity": "sha1-4aLWLN0jcjCirhGDkCexk4HjHos=", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha1-q4l4m4GKWHUrc9a1UjhiG3+qj9c=", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-subtokenize": { + "version": "2.1.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", + "integrity": "sha1-2K3lug8xl6HPaimZ+7/mNXoaGe4=", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha1-5dpJTo6ysHGg0I+zT2zv7GwKGbg=", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-types": { + "version": "2.0.2", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha1-8AIl9fWg68MlT5bDa2YFxLOTkI4=", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha1-1m+hjzpHB2eJMgubGvMr2G2fogI=", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha1-WpQpFeJrNy3A8OZ1MUmhbmscVgE=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha1-zds+5PnGRTDf9kAjZmHULLajFPU=", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha1-OQAtQYJXXVrwNv+hGBAPJSSy4qs=", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha1-vUhoegvjjtKWE5kQVgD4MglYYdE=", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha1-eTibTrG7LQA6m7qH1JLyvTe9xls=", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mlly": { + "version": "1.8.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/mlly/-/mlly-1.8.2.tgz", + "integrity": "sha1-5/eRmoLROxdEBWExFySaP0SdeLs=", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.16.0", + "pathe": "^2.0.3", + "pkg-types": "^1.3.1", + "ufo": "^1.6.3" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/ms/-/ms-2.1.3.tgz", + "integrity": "sha1-V0yBOM4dK1hh8LRFedut1gxmFbI=", + "license": "MIT" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/mz/-/mz-2.7.0.tgz", + "integrity": "sha1-lQCAV6Vsr63CvGPd5/n/aVWUjjI=", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.16", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha1-oE2OxLHxAAnS1TOUeu/kKTc3gWw=", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", + "dev": true, + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha1-tskbtHFy1p+Tz9fDV7u1KQGbX2o=", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha1-g3UmXiG8IND6WCwi4bE0hdbgAhM=", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/obug": { + "version": "2.1.4", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/obug/-/obug-2.1.4.tgz", + "integrity": "sha1-kJDYpUilIlF5FdKqaq6QcZesbPg=", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha1-WMjEQRblSEWtV/FKsQsDUzGErD8=", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha1-fqHBpdkddk+yghOciP4R4YKjpzQ=", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha1-4drMvnjQ0TiMoYxk/qOOPlfjcGs=", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha1-g8gxXGeFAF470CGDlBHJ4RDm2DQ=", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha1-TxRxoBCCeob5TP2bByfjbSZ95QU=", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha1-aR0nCeeMefrjoVZiJFLQB2LKqqI=", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-entities": { + "version": "4.0.2", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/parse-entities/-/parse-entities-4.0.2.tgz", + "integrity": "sha1-YdRvXtKOTuYundxD1rAQGIRD8Vk=", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "character-entities-legacy": "^3.0.0", + "character-reference-invalid": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "is-alphanumerical": "^2.0.0", + "is-decimal": "^2.0.0", + "is-hexadecimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/parse-entities/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha1-Ea9XsSfjJId3SEH3pOVOqxZtA8Q=", + "license": "MIT" + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha1-1+Ik+nI5nHoXUJn0X8KtAksF7AU=", + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha1-naGee+6NEt/wUT7Vt2lXeTvC6NQ=", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha1-UTvb4tO5XXdi6METfvoZXGxhtbM=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha1-WB9q3mWMu6ZaDTOA3ndTKVBU83U=", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha1-a+DQ7gKhDZ4N56mLrmXhgskGH4U=", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-to-regexp": { + "version": "8.4.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha1-eVxCDE98pFxbiHNm9iLuDJhSzM0=", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha1-PsvsVUIWhbcKnahyss/z4cvtFxY=", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha1-PTIa8+q5ObCDyPkpodEs2oHCa2s=", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha1-UepXoX2G9gX4EDlZX7xA7QalX6s=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha1-ZDtKGMQlfIplEEtz8wSc6aChXiI=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkce-challenge": { + "version": "5.0.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/pkce-challenge/-/pkce-challenge-5.0.1.tgz", + "integrity": "sha1-O0RGhlsXsXRems4gFqMfSN32Iw0=", + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/pkg-types": { + "version": "1.3.1", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/pkg-types/-/pkg-types-1.3.1.tgz", + "integrity": "sha1-vXzHCIEZJ3fu9TJsGd60bokJF98=", + "dev": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.1.8", + "mlly": "^1.7.4", + "pathe": "^2.0.1" + } + }, + "node_modules/plotly.js-dist-min": { + "version": "2.35.3", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/plotly.js-dist-min/-/plotly.js-dist-min-2.35.3.tgz", + "integrity": "sha1-uAx5VG/iHB9YVle1nYDK8aDY9WM=", + "license": "MIT" + }, + "node_modules/postcss": { + "version": "8.5.19", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/postcss/-/postcss-8.5.19.tgz", + "integrity": "sha1-Ra1c/eSZQI4gFHNII3VROBqSIDc=", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-load-config": { + "version": "6.0.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha1-b9fc2K6JutzxstZESJy6v4OqgJY=", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.1.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha1-3rxkidem5rDnYRiIzsiAM30xY5Y=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.9.5", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/prettier/-/prettier-3.9.5.tgz", + "integrity": "sha1-T+yXc24zudC2ILSJFP6TtTDoNa0=", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prismjs": { + "version": "1.30.0", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/prismjs/-/prismjs-1.30.0.tgz", + "integrity": "sha1-2XCZadnU4WQD9vNIxjVTsZ8Jdak=", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/property-information": { + "version": "7.2.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/property-information/-/property-information-7.2.0.tgz", + "integrity": "sha1-CAmzQmTplcC/zTInAooeNSEK+Ao=", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha1-8Z/mnOqzEe65S0LnDowgcPm6ECU=", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha1-AnQi4vrsCyXhVJw+G9gwm5EztuU=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.15.3", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/qs/-/qs-6.15.3.tgz", + "integrity": "sha1-doUhMqWO1cfA72fkRBubtdYGGzs=", + "license": "BSD-3-Clause", + "dependencies": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.3.0", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/range-parser/-/range-parser-1.3.0.tgz", + "integrity": "sha1-1/Gb6BK7YnIUcrRdO+IZ7wlXK0c=", + "license": "MIT", + "engines": { + "node": ">= 0.6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha1-PjraWuVWj5CV2EN2/TpJuPsAClE=", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/react/-/react-18.3.1.tgz", + "integrity": "sha1-SauJIAnFOTNiW9FrJTP8dUyrKJE=", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha1-wiZdeVEbV9R5s90/36UVNklMXLQ=", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-i18next": { + "version": "17.0.10", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/react-i18next/-/react-i18next-17.0.10.tgz", + "integrity": "sha1-rv30MkdIyxBc/vke00fqsSmGVpg=", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.2", + "html-parse-stringify": "^3.0.1", + "use-sync-external-store": "^1.6.0" + }, + "peerDependencies": { + "i18next": ">= 26.2.0", + "react": ">= 16.8.0", + "typescript": "^5 || ^6 || ^7" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/react-markdown": { + "version": "10.1.0", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/react-markdown/-/react-markdown-10.1.0.tgz", + "integrity": "sha1-4ivCD63bwHYFwVKEJVZTwPO61co=", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "hast-util-to-jsx-runtime": "^2.0.0", + "html-url-attributes": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.0.0", + "unified": "^11.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + }, + "peerDependencies": { + "@types/react": ">=18", + "react": ">=18" + } + }, + "node_modules/react-router": { + "version": "7.18.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/react-router/-/react-router-7.18.1.tgz", + "integrity": "sha1-YSWdFZS5XBrOKZ7kxXRTVw8MIvE=", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.18.1", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/react-router-dom/-/react-router-dom-7.18.1.tgz", + "integrity": "sha1-DRsTjikTkwWa1IHD4Q42Y4WpeKQ=", + "license": "MIT", + "dependencies": { + "react-router": "7.18.1" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/react-router/node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha1-O7m9/II2nbnC9pyTycPOsxDIizw=", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/react-syntax-highlighter": { + "version": "16.1.1", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/react-syntax-highlighter/-/react-syntax-highlighter-16.1.1.tgz", + "integrity": "sha1-koRZhV03X1z8jmRgceINVBzry1I=", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.4", + "highlight.js": "^10.4.1", + "highlightjs-vue": "^1.0.0", + "lowlight": "^1.17.0", + "prismjs": "^1.30.0", + "refractor": "^5.0.0" + }, + "engines": { + "node": ">= 16.20.2" + }, + "peerDependencies": { + "react": ">= 0.14.0" + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha1-64WAFDX78qfuWPGeCSGwaPxplI0=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/refractor": { + "version": "5.0.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/refractor/-/refractor-5.0.0.tgz", + "integrity": "sha1-hdrwRIptlH9TYXlusiwxcz1h2QQ=", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/prismjs": "^1.0.0", + "hastscript": "^9.0.0", + "parse-entities": "^4.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/rehype-katex": { + "version": "7.0.1", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/rehype-katex/-/rehype-katex-7.0.1.tgz", + "integrity": "sha1-gy5tevJ0SiKJgdGw/olIOp58k6E=", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/katex": "^0.16.0", + "hast-util-from-html-isomorphic": "^2.0.0", + "hast-util-to-text": "^4.0.0", + "katex": "^0.16.0", + "unist-util-visit-parents": "^6.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-katex/node_modules/katex": { + "version": "0.16.47", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/katex/-/katex-0.16.47.tgz", + "integrity": "sha1-ChOkLC3rT3TmHxYtRAuRZaVIAw8=", + "funding": [ + "https://opencollective.com/katex", + "https://github.com/sponsors/katex" + ], + "license": "MIT", + "dependencies": { + "commander": "^8.3.0" + }, + "bin": { + "katex": "cli.js" + } + }, + "node_modules/remark-gfm": { + "version": "4.0.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/remark-gfm/-/remark-gfm-4.0.1.tgz", + "integrity": "sha1-MyJ7KnQ5dnDTV78FwJjq+FE/DWs=", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-gfm": "^3.0.0", + "micromark-extension-gfm": "^3.0.0", + "remark-parse": "^11.0.0", + "remark-stringify": "^11.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-math": { + "version": "6.0.0", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/remark-math/-/remark-math-6.0.0.tgz", + "integrity": "sha1-Cs33RnXxwZX+pu//p4WC9+1/wNc=", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-math": "^3.0.0", + "micromark-extension-math": "^3.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-parse": { + "version": "11.0.0", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/remark-parse/-/remark-parse-11.0.0.tgz", + "integrity": "sha1-qmB0P8s36/awaSBOtNowTkDbRaE=", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-rehype": { + "version": "11.1.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/remark-rehype/-/remark-rehype-11.1.2.tgz", + "integrity": "sha1-Kt2q3agMqb2aoNp2PnTRYydoOzc=", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "mdast-util-to-hast": "^13.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-stringify": { + "version": "11.0.0", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/remark-stringify/-/remark-stringify-11.0.0.tgz", + "integrity": "sha1-TFsB3XEcJp3xqq4RdD634udjb9M=", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-to-markdown": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha1-iaf92TgmEmcxjq/hT5wy5ZjDaQk=", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha1-w1IlhD3493bfIcV1V7wIfp39/Gk=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/rimraf": { + "version": "6.1.3", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/rimraf/-/rimraf-6.1.3.tgz", + "integrity": "sha1-r77iNrO9K+Mx1OfORJO6wXGJga8=", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "glob": "^13.0.3", + "package-json-from-dist": "^1.0.1" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/robust-predicates": { + "version": "3.0.3", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/robust-predicates/-/robust-predicates-3.0.3.tgz", + "integrity": "sha1-EJkGGzNJ4sWr7GwqsKzUQNJNQGI=", + "license": "Unlicense" + }, + "node_modules/rolldown": { + "version": "1.1.5", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/rolldown/-/rolldown-1.1.5.tgz", + "integrity": "sha1-M5quJQhENR/FW3TiZS0+vW+6OJ0=", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.139.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.1.5", + "@rolldown/binding-darwin-arm64": "1.1.5", + "@rolldown/binding-darwin-x64": "1.1.5", + "@rolldown/binding-freebsd-x64": "1.1.5", + "@rolldown/binding-linux-arm-gnueabihf": "1.1.5", + "@rolldown/binding-linux-arm64-gnu": "1.1.5", + "@rolldown/binding-linux-arm64-musl": "1.1.5", + "@rolldown/binding-linux-ppc64-gnu": "1.1.5", + "@rolldown/binding-linux-s390x-gnu": "1.1.5", + "@rolldown/binding-linux-x64-gnu": "1.1.5", + "@rolldown/binding-linux-x64-musl": "1.1.5", + "@rolldown/binding-openharmony-arm64": "1.1.5", + "@rolldown/binding-wasm32-wasi": "1.1.5", + "@rolldown/binding-win32-arm64-msvc": "1.1.5", + "@rolldown/binding-win32-x64-msvc": "1.1.5" + } + }, + "node_modules/rollup": { + "version": "4.62.2", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/rollup/-/rollup-4.62.2.tgz", + "integrity": "sha1-2Q/Ey4EfBxMDyJC3eVlWNPNflUE=", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.62.2", + "@rollup/rollup-android-arm64": "4.62.2", + "@rollup/rollup-darwin-arm64": "4.62.2", + "@rollup/rollup-darwin-x64": "4.62.2", + "@rollup/rollup-freebsd-arm64": "4.62.2", + "@rollup/rollup-freebsd-x64": "4.62.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", + "@rollup/rollup-linux-arm-musleabihf": "4.62.2", + "@rollup/rollup-linux-arm64-gnu": "4.62.2", + "@rollup/rollup-linux-arm64-musl": "4.62.2", + "@rollup/rollup-linux-loong64-gnu": "4.62.2", + "@rollup/rollup-linux-loong64-musl": "4.62.2", + "@rollup/rollup-linux-ppc64-gnu": "4.62.2", + "@rollup/rollup-linux-ppc64-musl": "4.62.2", + "@rollup/rollup-linux-riscv64-gnu": "4.62.2", + "@rollup/rollup-linux-riscv64-musl": "4.62.2", + "@rollup/rollup-linux-s390x-gnu": "4.62.2", + "@rollup/rollup-linux-x64-gnu": "4.62.2", + "@rollup/rollup-linux-x64-musl": "4.62.2", + "@rollup/rollup-openbsd-x64": "4.62.2", + "@rollup/rollup-openharmony-arm64": "4.62.2", + "@rollup/rollup-win32-arm64-msvc": "4.62.2", + "@rollup/rollup-win32-ia32-msvc": "4.62.2", + "@rollup/rollup-win32-x64-gnu": "4.62.2", + "@rollup/rollup-win32-x64-msvc": "4.62.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/router/-/router-2.2.0.tgz", + "integrity": "sha1-AZvmILcRyHZBFnzHm5kJDwCxRu8=", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/rw": { + "version": "1.3.3", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/rw/-/rw-1.3.3.tgz", + "integrity": "sha1-P4Yt+pGrdmsUiF700BEkv9oHT7Q=", + "license": "BSD-3-Clause" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha1-RPoWGwGHuVSd2Eu5GAL5vYOFzWo=", + "license": "MIT" + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha1-QUumSjsoKJLpRM8hCOzAeNEVzcM=", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/semver/-/semver-7.8.5.tgz", + "integrity": "sha1-ObZGA33VDBT7RR5+TKxY7YuGP2k=", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/send/-/send-1.2.1.tgz", + "integrity": "sha1-nqt0O4dPNVD0CiaGe/KGrWDT8+0=", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha1-fxhqSk5fW2Y616QpT/G/N88OmKk=", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha1-zNCGc6muXS5E6iot4lCJ5nx+32g=", + "license": "MIT" + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha1-ZsmiSnP5/CjL5msJ/tPTPcrxtCQ=", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha1-zNCvT4g1+9wmW4JGGq8MNmY/NOo=", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha1-rhbxZE2HPsrYQ7AwexQzYtTEIXI=", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha1-6gLGLgXcS+pn1EQvD7ce4ZL44Ks=", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha1-wuC1oUpUCuvuO7xsP4ZmzJtQkSc=", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha1-1rtrN5Asb+9RdOX1M/q0xzKib0I=", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha1-Ed2hnVNo5Azp7CvcH7DsvAeQ7Oo=", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha1-MudscLeXJOO7Vny51UPrhYzPrzA=", + "dev": true, + "license": "ISC" + }, + "node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha1-o2WKuH5bZCnIofO6AIPUxhyj7wI=", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha1-HOVlD93YerwJnto33P8CTCZnrkY=", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha1-Hs2dI1CjhEVyw/SjErzrAYNIhZ8=", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha1-Gsig2Ug4SNFpXkGLbQMaPDzmjjs=", + "dev": true, + "license": "MIT" + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha1-j3XuzvdlteHPzcCA2llAntQk44I=", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/std-env": { + "version": "4.2.0", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/std-env/-/std-env-4.2.0.tgz", + "integrity": "sha1-jr4OxgSFZoq0ciezEvQlTN+AydM=", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha1-tbuOIWXOJ11NQ0dt0nAK2Qkdttw=", + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha1-s7ee9fJ3zErHPK6wI2xbqTmzpPM=", + "license": "MIT", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha1-0iomlSKDamJ6+NBLXD/Sx/o+MuM=", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha1-MfEoGzgyYwQ0gxwxDAHMzajL4AY=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/style-mod": { + "version": "4.1.3", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/style-mod/-/style-mod-4.1.3.tgz", + "integrity": "sha1-bpASJVu3mb2sN+KI92cbXXG/n3M=", + "license": "MIT" + }, + "node_modules/style-to-js": { + "version": "1.1.21", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/style-to-js/-/style-to-js-1.1.21.tgz", + "integrity": "sha1-KQiUEYf4V+eeKOnNeACLmgs+Do0=", + "license": "MIT", + "dependencies": { + "style-to-object": "1.0.14" + } + }, + "node_modules/style-to-object": { + "version": "1.0.14", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/style-to-object/-/style-to-object-1.0.14.tgz", + "integrity": "sha1-HSLw5yZruMbYyuXK9OxPAF4I9hE=", + "license": "MIT", + "dependencies": { + "inline-style-parser": "0.2.7" + } + }, + "node_modules/sucrase": { + "version": "3.35.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha1-RhnqUDk/6L0K5QccJqvZsuNGv+E=", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/sucrase/node_modules/commander": { + "version": "4.1.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/commander/-/commander-4.1.1.tgz", + "integrity": "sha1-n9YCvZNilOnp70aj9NaWQESxgGg=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha1-G33NyzK4E4gBs+R4umpRyqiWSNo=", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha1-iTLmhqQGYDigFt2eLKRq3Zg4qV8=", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha1-GhkY1ALY/D+Y+/I02wvMjMEOlyY=", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha1-EDyfi6bXI3pHq23R3P93JRhjQms=", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha1-lBeU5leoXklld5lcbu9m9T9Cs9I=", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha1-ViqabJ6ys7Ej05cZ+a9btE/NdjE=", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha1-HYpiOJP5XPCi3bnl0RFQ4ZFAlCE=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha1-FkjESq58jZiKMmAY7XL1tN0DkuQ=", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha1-O+NDIaiKgg7RvYDfqjPkefu43TU=", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/topojson-client": { + "version": "3.1.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/topojson-client/-/topojson-client-3.1.0.tgz", + "integrity": "sha1-Iuix7QiiuSL+60r29Ttu8JpGe5k=", + "license": "ISC", + "dependencies": { + "commander": "2" + }, + "bin": { + "topo2geo": "bin/topo2geo", + "topomerge": "bin/topomerge", + "topoquantize": "bin/topoquantize" + } + }, + "node_modules/topojson-client/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/commander/-/commander-2.20.3.tgz", + "integrity": "sha1-/UhehMA+tIgcIHIrpIA16FMa6zM=", + "license": "MIT" + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha1-TKCakJLIi3OnzcXooBtQeweQoMw=", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha1-2ALjMqB9+GHEiALAQyEBexvYczg=", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/trough": { + "version": "2.2.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/trough/-/trough-2.2.0.tgz", + "integrity": "sha1-lKYL1r03XBUsHfkRpLEdWwJW9Q8=", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha1-Ss1KFV4ic0mQpe0f6el/ETvLN8E=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha1-eE/T1nlyK8EDsbS4AwvN212yppk=", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/tslib": { + "version": "2.3.0", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/tslib/-/tslib-2.3.0.tgz", + "integrity": "sha1-gDuM2rPhK6WBpMpByIObuw2ssJ4=", + "license": "0BSD" + }, + "node_modules/tsup": { + "version": "8.5.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/tsup/-/tsup-8.5.1.tgz", + "integrity": "sha1-qceodbkzRL33BgDe3XjnD4jsmmU=", + "dev": true, + "license": "MIT", + "dependencies": { + "bundle-require": "^5.1.0", + "cac": "^6.7.14", + "chokidar": "^4.0.3", + "consola": "^3.4.0", + "debug": "^4.4.0", + "esbuild": "^0.27.0", + "fix-dts-default-cjs-exports": "^1.0.0", + "joycon": "^3.1.1", + "picocolors": "^1.1.1", + "postcss-load-config": "^6.0.1", + "resolve-from": "^5.0.0", + "rollup": "^4.34.8", + "source-map": "^0.7.6", + "sucrase": "^3.35.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.11", + "tree-kill": "^1.2.2" + }, + "bin": { + "tsup": "dist/cli-default.js", + "tsup-node": "dist/cli-node.js" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@microsoft/api-extractor": "^7.36.0", + "@swc/core": "^1", + "postcss": "^8.4.12", + "typescript": ">=4.5.0" + }, + "peerDependenciesMeta": { + "@microsoft/api-extractor": { + "optional": true + }, + "@swc/core": { + "optional": true + }, + "postcss": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha1-B7ggO/pwVsBlcFDjzNLDdzC6uPE=", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-is": { + "version": "2.1.0", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/type-is/-/type-is-2.1.0.tgz", + "integrity": "sha1-cdGnBTKTWC4WrJ8+uvGrmqSeVXA=", + "license": "MIT", + "dependencies": { + "content-type": "^2.0.0", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/type-is/node_modules/content-type": { + "version": "2.0.0", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha1-L7Pt5p3/oK94ynxM51iWgGOLVt8=", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha1-W09Z4VMQqxeiFvXWz1PuR27eZw8=", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.64.0", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/typescript-eslint/-/typescript-eslint-8.64.0.tgz", + "integrity": "sha1-SYTa5N6dyL+JKs9cOU0KKl8Iw+E=", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.64.0", + "@typescript-eslint/parser": "8.64.0", + "@typescript-eslint/typescript-estree": "8.64.0", + "@typescript-eslint/utils": "8.64.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/ufo": { + "version": "1.6.4", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/ufo/-/ufo-1.6.4.tgz", + "integrity": "sha1-eo+4dfzGOC0sfQs2knOLBQCpJGc=", + "dev": true, + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha1-aR0ArzkJvpOn+qE75hs6W1DvEss=", + "dev": true, + "license": "MIT" + }, + "node_modules/unified": { + "version": "11.0.5", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/unified/-/unified-11.0.5.tgz", + "integrity": "sha1-9mZ3YQpcCp7pDKsrjU1mA3Am2eE=", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "bail": "^2.0.0", + "devlop": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-find-after": { + "version": "5.0.0", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/unist-util-find-after/-/unist-util-find-after-5.0.0.tgz", + "integrity": "sha1-P8zBsIa1bzTIt5jh/5C1xURo6JY=", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-is": { + "version": "6.0.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/unist-util-is/-/unist-util-is-6.0.1.tgz", + "integrity": "sha1-0KP4by3Q23rNfYwkeAgLXGf5xqk=", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha1-Z48gq1yhIHqX1+qKOINzyc+Ja+Q=", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-remove-position": { + "version": "5.0.0", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/unist-util-remove-position/-/unist-util-remove-position-5.0.0.tgz", + "integrity": "sha1-/qaKJWWECclGBAi8a0mRuWW1IWM=", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-visit": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha1-RJxuIaiA4IVb9aq63rOnQDFKusI=", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.1.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/unist-util-visit/-/unist-util-visit-5.1.0.tgz", + "integrity": "sha1-mioosKp2oV4NpwoIpYY6LwYOJGg=", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.2", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", + "integrity": "sha1-d333+5hlLOFrS3zZmdChpA76OgI=", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha1-mxpSWVIlhZ5V9mnZKPiMbFfyp34=", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha1-sXS/plyytSZzLZ8qwKQIAnh28y0=", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/vary/-/vary-1.1.2.tgz", + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vega": { + "version": "6.2.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/vega/-/vega-6.2.0.tgz", + "integrity": "sha1-NMLeg7AOcB4EAClziybx7JkvMn8=", + "license": "BSD-3-Clause", + "dependencies": { + "vega-crossfilter": "~5.1.0", + "vega-dataflow": "~6.1.0", + "vega-encode": "~5.1.0", + "vega-event-selector": "~4.0.0", + "vega-expression": "~6.1.0", + "vega-force": "~5.1.0", + "vega-format": "~2.1.0", + "vega-functions": "~6.1.0", + "vega-geo": "~5.1.0", + "vega-hierarchy": "~5.1.0", + "vega-label": "~2.1.0", + "vega-loader": "~5.1.0", + "vega-parser": "~7.1.0", + "vega-projection": "~2.1.0", + "vega-regression": "~2.1.0", + "vega-runtime": "~7.1.0", + "vega-scale": "~8.1.0", + "vega-scenegraph": "~5.1.0", + "vega-statistics": "~2.0.0", + "vega-time": "~3.1.0", + "vega-transforms": "~5.1.0", + "vega-typings": "~2.1.0", + "vega-util": "~2.1.0", + "vega-view": "~6.1.0", + "vega-view-transforms": "~5.1.0", + "vega-voronoi": "~5.1.0", + "vega-wordcloud": "~5.1.0" + }, + "funding": { + "url": "https://app.hubspot.com/payments/GyPC972GD9Rt" + } + }, + "node_modules/vega-canvas": { + "version": "2.0.0", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/vega-canvas/-/vega-canvas-2.0.0.tgz", + "integrity": "sha1-Rwneto+bT9dHWVe+2Z8Ww428B7g=", + "license": "BSD-3-Clause" + }, + "node_modules/vega-crossfilter": { + "version": "5.1.0", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/vega-crossfilter/-/vega-crossfilter-5.1.0.tgz", + "integrity": "sha1-9MVtngwxcFyuQc0ONavN7iDAxIM=", + "license": "BSD-3-Clause", + "dependencies": { + "d3-array": "^3.2.4", + "vega-dataflow": "^6.1.0", + "vega-util": "^2.1.0" + } + }, + "node_modules/vega-dataflow": { + "version": "6.1.0", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/vega-dataflow/-/vega-dataflow-6.1.0.tgz", + "integrity": "sha1-H8SOpru+AC1FoaSO7meuoJelfFU=", + "license": "BSD-3-Clause", + "dependencies": { + "vega-format": "^2.1.0", + "vega-loader": "^5.1.0", + "vega-util": "^2.1.0" + } + }, + "node_modules/vega-embed": { + "version": "7.1.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/vega-embed/-/vega-embed-7.1.0.tgz", + "integrity": "sha1-QoQyuiz1uwlhwNNI956E7Kn8jPs=", + "license": "BSD-3-Clause", + "dependencies": { + "fast-json-patch": "^3.1.1", + "json-stringify-pretty-compact": "^4.0.0", + "semver": "^7.7.2", + "tslib": "^2.8.1", + "vega-interpreter": "^2.0.0", + "vega-schema-url-parser": "^3.0.2", + "vega-themes": "3.0.0", + "vega-tooltip": "1.0.0" + }, + "funding": { + "url": "https://app.hubspot.com/payments/GyPC972GD9Rt" + }, + "peerDependencies": { + "vega": "*", + "vega-lite": "*" + } + }, + "node_modules/vega-embed/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha1-YS7+TtI11Wfoq6Xypfq3AoCt6D8=", + "license": "0BSD" + }, + "node_modules/vega-encode": { + "version": "5.1.0", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/vega-encode/-/vega-encode-5.1.0.tgz", + "integrity": "sha1-BfVriYgi4J35alyn8QF7n5ocTTs=", + "license": "BSD-3-Clause", + "dependencies": { + "d3-array": "^3.2.4", + "d3-interpolate": "^3.0.1", + "vega-dataflow": "^6.1.0", + "vega-scale": "^8.1.0", + "vega-util": "^2.1.0" + } + }, + "node_modules/vega-event-selector": { + "version": "4.0.0", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/vega-event-selector/-/vega-event-selector-4.0.0.tgz", + "integrity": "sha1-Ql6fJnHoWKGkW0tqf8RSygsiq78=", + "license": "BSD-3-Clause" + }, + "node_modules/vega-expression": { + "version": "6.1.0", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/vega-expression/-/vega-expression-6.1.0.tgz", + "integrity": "sha1-bONYo5ublTgGv/IA9vhPRBY8njg=", + "license": "BSD-3-Clause", + "dependencies": { + "@types/estree": "^1.0.8", + "vega-util": "^2.1.0" + } + }, + "node_modules/vega-force": { + "version": "5.1.0", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/vega-force/-/vega-force-5.1.0.tgz", + "integrity": "sha1-qnz47b4q47raBw80NWXfuEHlAak=", + "license": "BSD-3-Clause", + "dependencies": { + "d3-force": "^3.0.0", + "vega-dataflow": "^6.1.0", + "vega-util": "^2.1.0" + } + }, + "node_modules/vega-format": { + "version": "2.1.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/vega-format/-/vega-format-2.1.0.tgz", + "integrity": "sha1-RlLH7J+xt/+aLFDc1Jija6YUb9o=", + "license": "BSD-3-Clause", + "dependencies": { + "d3-array": "^3.2.4", + "d3-format": "^3.1.0", + "d3-time-format": "^4.1.0", + "vega-time": "^3.1.0", + "vega-util": "^2.1.0" + } + }, + "node_modules/vega-functions": { + "version": "6.1.1", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/vega-functions/-/vega-functions-6.1.1.tgz", + "integrity": "sha1-XU6XRqrd4rO3DY2j5jOIktBc2dI=", + "license": "BSD-3-Clause", + "dependencies": { + "d3-array": "^3.2.4", + "d3-color": "^3.1.0", + "d3-geo": "^3.1.1", + "vega-dataflow": "^6.1.0", + "vega-expression": "^6.1.0", + "vega-scale": "^8.1.0", + "vega-scenegraph": "^5.1.0", + "vega-selections": "^6.1.0", + "vega-statistics": "^2.0.0", + "vega-time": "^3.1.0", + "vega-util": "^2.1.0" + } + }, + "node_modules/vega-geo": { + "version": "5.1.0", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/vega-geo/-/vega-geo-5.1.0.tgz", + "integrity": "sha1-2P5q6RKtJ80rHCH1RadMB9oJNYk=", + "license": "BSD-3-Clause", + "dependencies": { + "d3-array": "^3.2.4", + "d3-color": "^3.1.0", + "d3-geo": "^3.1.1", + "vega-canvas": "^2.0.0", + "vega-dataflow": "^6.1.0", + "vega-projection": "^2.1.0", + "vega-statistics": "^2.0.0", + "vega-util": "^2.1.0" + } + }, + "node_modules/vega-hierarchy": { + "version": "5.1.0", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/vega-hierarchy/-/vega-hierarchy-5.1.0.tgz", + "integrity": "sha1-Qjdw3Ry0aENw8jpojcW22tE5nb8=", + "license": "BSD-3-Clause", + "dependencies": { + "d3-hierarchy": "^3.1.2", + "vega-dataflow": "^6.1.0", + "vega-util": "^2.1.0" + } + }, + "node_modules/vega-interpreter": { + "version": "2.2.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/vega-interpreter/-/vega-interpreter-2.2.1.tgz", + "integrity": "sha1-lH2PKIelnRJK3diE0BsC9lc/uFE=", + "license": "BSD-3-Clause", + "dependencies": { + "vega-util": "^2.1.0" + } + }, + "node_modules/vega-label": { + "version": "2.1.0", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/vega-label/-/vega-label-2.1.0.tgz", + "integrity": "sha1-vZd80U6bBi/OMVk6LbKBmqnvssk=", + "license": "BSD-3-Clause", + "dependencies": { + "vega-canvas": "^2.0.0", + "vega-dataflow": "^6.1.0", + "vega-scenegraph": "^5.1.0", + "vega-util": "^2.1.0" + } + }, + "node_modules/vega-lite": { + "version": "6.4.3", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/vega-lite/-/vega-lite-6.4.3.tgz", + "integrity": "sha1-mXPTr+E5H7CIB7wlheEp4tr3o2s=", + "license": "BSD-3-Clause", + "dependencies": { + "json-stringify-pretty-compact": "~4.0.0", + "tslib": "~2.8.1", + "vega-event-selector": "~4.0.0", + "vega-expression": "~6.1.0", + "vega-util": "~2.1.0", + "yargs": "~18.0.0" + }, + "bin": { + "vl2pdf": "bin/vl2pdf", + "vl2png": "bin/vl2png", + "vl2svg": "bin/vl2svg", + "vl2vg": "bin/vl2vg" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://app.hubspot.com/payments/GyPC972GD9Rt" + }, + "peerDependencies": { + "vega": "^6.0.0" + } + }, + "node_modules/vega-lite/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha1-YS7+TtI11Wfoq6Xypfq3AoCt6D8=", + "license": "0BSD" + }, + "node_modules/vega-loader": { + "version": "5.1.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/vega-loader/-/vega-loader-5.1.0.tgz", + "integrity": "sha1-aTePxNRujUVzrTCPdkZOZrAleeY=", + "license": "BSD-3-Clause", + "dependencies": { + "d3-dsv": "^3.0.1", + "topojson-client": "^3.1.0", + "vega-format": "^2.1.0", + "vega-util": "^2.1.0" + } + }, + "node_modules/vega-parser": { + "version": "7.1.0", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/vega-parser/-/vega-parser-7.1.0.tgz", + "integrity": "sha1-IO4OcKbs24yzTvFt7tSErWjECFA=", + "license": "BSD-3-Clause", + "dependencies": { + "vega-dataflow": "^6.1.0", + "vega-event-selector": "^4.0.0", + "vega-functions": "^6.1.0", + "vega-scale": "^8.1.0", + "vega-util": "^2.1.0" + } + }, + "node_modules/vega-projection": { + "version": "2.1.0", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/vega-projection/-/vega-projection-2.1.0.tgz", + "integrity": "sha1-zkYpHveKdBjHVnkQMpbWL0mvrBQ=", + "license": "BSD-3-Clause", + "dependencies": { + "d3-geo": "^3.1.1", + "d3-geo-projection": "^4.0.0", + "vega-scale": "^8.1.0" + } + }, + "node_modules/vega-regression": { + "version": "2.1.0", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/vega-regression/-/vega-regression-2.1.0.tgz", + "integrity": "sha1-0/0QPpegruVa4qeO2BWI+13LngM=", + "license": "BSD-3-Clause", + "dependencies": { + "d3-array": "^3.2.4", + "vega-dataflow": "^6.1.0", + "vega-statistics": "^2.0.0", + "vega-util": "^2.1.0" + } + }, + "node_modules/vega-runtime": { + "version": "7.1.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/vega-runtime/-/vega-runtime-7.1.0.tgz", + "integrity": "sha1-GVnWFoY4+Fvc5NFXEXrKatH2n6w=", + "license": "BSD-3-Clause", + "dependencies": { + "vega-dataflow": "^6.1.0", + "vega-util": "^2.1.0" + } + }, + "node_modules/vega-scale": { + "version": "8.1.0", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/vega-scale/-/vega-scale-8.1.0.tgz", + "integrity": "sha1-oGs6qNYK5GrY89ierg506z0SAOM=", + "license": "BSD-3-Clause", + "dependencies": { + "d3-array": "^3.2.4", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-scale-chromatic": "^3.1.0", + "vega-time": "^3.1.0", + "vega-util": "^2.1.0" + } + }, + "node_modules/vega-scenegraph": { + "version": "5.1.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/vega-scenegraph/-/vega-scenegraph-5.1.0.tgz", + "integrity": "sha1-OzwNhxeZ/oS8VjJW17nVS8LhM2g=", + "license": "BSD-3-Clause", + "dependencies": { + "d3-path": "^3.1.0", + "d3-shape": "^3.2.0", + "vega-canvas": "^2.0.0", + "vega-loader": "^5.1.0", + "vega-scale": "^8.1.0", + "vega-util": "^2.1.0" + } + }, + "node_modules/vega-schema-url-parser": { + "version": "3.0.2", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/vega-schema-url-parser/-/vega-schema-url-parser-3.0.2.tgz", + "integrity": "sha1-lfCmrX/JNBr8BUlcGgdMYvpxxFY=", + "license": "BSD-3-Clause" + }, + "node_modules/vega-selections": { + "version": "6.1.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/vega-selections/-/vega-selections-6.1.2.tgz", + "integrity": "sha1-Nkbbap/B1yWWm4tYQeXTM8Hw+AM=", + "license": "BSD-3-Clause", + "dependencies": { + "d3-array": "3.2.4", + "vega-expression": "^6.1.0", + "vega-util": "^2.1.0" + } + }, + "node_modules/vega-statistics": { + "version": "2.0.0", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/vega-statistics/-/vega-statistics-2.0.0.tgz", + "integrity": "sha1-nJY2wgaCrpjoiH+Pqw6CwkZqc2o=", + "license": "BSD-3-Clause", + "dependencies": { + "d3-array": "^3.2.4" + } + }, + "node_modules/vega-themes": { + "version": "3.0.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/vega-themes/-/vega-themes-3.0.0.tgz", + "integrity": "sha1-lvP3nDkvwwIOXRLvWP5k9RbTbh4=", + "license": "BSD-3-Clause", + "funding": { + "url": "https://app.hubspot.com/payments/GyPC972GD9Rt" + }, + "peerDependencies": { + "vega": "*", + "vega-lite": "*" + } + }, + "node_modules/vega-time": { + "version": "3.1.0", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/vega-time/-/vega-time-3.1.0.tgz", + "integrity": "sha1-TiDF1g4/foJ6M9spvUhV9AoK48s=", + "license": "BSD-3-Clause", + "dependencies": { + "d3-array": "^3.2.4", + "d3-time": "^3.1.0", + "vega-util": "^2.1.0" + } + }, + "node_modules/vega-tooltip": { + "version": "1.0.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/vega-tooltip/-/vega-tooltip-1.0.0.tgz", + "integrity": "sha1-xoKcm831voU4UDB2ZcnyEyEIP/I=", + "license": "BSD-3-Clause", + "dependencies": { + "vega-util": "^2.0.0" + }, + "funding": { + "url": "https://app.hubspot.com/payments/GyPC972GD9Rt" + } + }, + "node_modules/vega-transforms": { + "version": "5.1.0", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/vega-transforms/-/vega-transforms-5.1.0.tgz", + "integrity": "sha1-TpXNfEdzqlYJKNEDhaDTPqJ0jKo=", + "license": "BSD-3-Clause", + "dependencies": { + "d3-array": "^3.2.4", + "vega-dataflow": "^6.1.0", + "vega-statistics": "^2.0.0", + "vega-time": "^3.1.0", + "vega-util": "^2.1.0" + } + }, + "node_modules/vega-typings": { + "version": "2.1.0", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/vega-typings/-/vega-typings-2.1.0.tgz", + "integrity": "sha1-HB/lSMDwCZeCAkat4NPYE7h7/XY=", + "license": "BSD-3-Clause", + "dependencies": { + "@types/geojson": "7946.0.16", + "vega-event-selector": "^4.0.0", + "vega-expression": "^6.1.0", + "vega-util": "^2.1.0" + } + }, + "node_modules/vega-util": { + "version": "2.1.1", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/vega-util/-/vega-util-2.1.1.tgz", + "integrity": "sha1-0fSU4C/dK2oWeadAuFJUuGTN1Zc=", + "license": "BSD-3-Clause" + }, + "node_modules/vega-view": { + "version": "6.1.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/vega-view/-/vega-view-6.1.0.tgz", + "integrity": "sha1-VZb3jF68qNy1f+ykD9McuCZf0E4=", + "license": "BSD-3-Clause", + "dependencies": { + "d3-array": "^3.2.4", + "d3-timer": "^3.0.1", + "vega-dataflow": "^6.1.0", + "vega-format": "^2.1.0", + "vega-functions": "^6.1.0", + "vega-runtime": "^7.1.0", + "vega-scenegraph": "^5.1.0", + "vega-util": "^2.1.0" + } + }, + "node_modules/vega-view-transforms": { + "version": "5.1.0", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/vega-view-transforms/-/vega-view-transforms-5.1.0.tgz", + "integrity": "sha1-HzH3Xvz5mziWnnUAQ625Ivzsbz4=", + "license": "BSD-3-Clause", + "dependencies": { + "vega-dataflow": "^6.1.0", + "vega-scenegraph": "^5.1.0", + "vega-util": "^2.1.0" + } + }, + "node_modules/vega-voronoi": { + "version": "5.1.0", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/vega-voronoi/-/vega-voronoi-5.1.0.tgz", + "integrity": "sha1-kpVrnXjwbjkYlw/ITQaXTiS59S8=", + "license": "BSD-3-Clause", + "dependencies": { + "d3-delaunay": "^6.0.4", + "vega-dataflow": "^6.1.0", + "vega-util": "^2.1.0" + } + }, + "node_modules/vega-wordcloud": { + "version": "5.1.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/vega-wordcloud/-/vega-wordcloud-5.1.0.tgz", + "integrity": "sha1-eqjcv2yDsZP+cftkEL4VrSxyheY=", + "license": "BSD-3-Clause", + "dependencies": { + "vega-canvas": "^2.0.0", + "vega-dataflow": "^6.1.0", + "vega-scale": "^8.1.0", + "vega-statistics": "^2.0.0", + "vega-util": "^2.1.0" + } + }, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha1-NlKrHEllMYUr9VprrFevmB68OKs=", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-location": { + "version": "5.0.3", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/vfile-location/-/vfile-location-5.0.3.tgz", + "integrity": "sha1-y56s0g8rZCbRlFHg6vo9CoRiJcM=", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.3", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha1-h7RN3de3DwZBwuPtCGS6c+LqjfQ=", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vite": { + "version": "8.1.5", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/vite/-/vite-8.1.5.tgz", + "integrity": "sha1-zP/OPuSHsYhiI7JOuye5FnSCfTA=", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.5", + "postcss": "^8.5.17", + "rolldown": "~1.1.5", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.3.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } } }, - "node_modules/vite/node_modules/esbuild": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", - "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "node_modules/vite-plugin-singlefile": { + "version": "2.3.3", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/vite-plugin-singlefile/-/vite-plugin-singlefile-2.3.3.tgz", + "integrity": "sha1-6FmupMDEt0/LumuvUn6I8q0J3mk=", "dev": true, - "hasInstallScript": true, "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" + "dependencies": { + "micromatch": "^4.0.8" }, "engines": { - "node": ">=12" + "node": ">18.0.0" }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.21.5", - "@esbuild/android-arm": "0.21.5", - "@esbuild/android-arm64": "0.21.5", - "@esbuild/android-x64": "0.21.5", - "@esbuild/darwin-arm64": "0.21.5", - "@esbuild/darwin-x64": "0.21.5", - "@esbuild/freebsd-arm64": "0.21.5", - "@esbuild/freebsd-x64": "0.21.5", - "@esbuild/linux-arm": "0.21.5", - "@esbuild/linux-arm64": "0.21.5", - "@esbuild/linux-ia32": "0.21.5", - "@esbuild/linux-loong64": "0.21.5", - "@esbuild/linux-mips64el": "0.21.5", - "@esbuild/linux-ppc64": "0.21.5", - "@esbuild/linux-riscv64": "0.21.5", - "@esbuild/linux-s390x": "0.21.5", - "@esbuild/linux-x64": "0.21.5", - "@esbuild/netbsd-x64": "0.21.5", - "@esbuild/openbsd-x64": "0.21.5", - "@esbuild/sunos-x64": "0.21.5", - "@esbuild/win32-arm64": "0.21.5", - "@esbuild/win32-ia32": "0.21.5", - "@esbuild/win32-x64": "0.21.5" + "peerDependencies": { + "rollup": "^4.59.0", + "vite": "^5.4.21 || ^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } } }, "node_modules/vitest": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.9.tgz", - "integrity": "sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/expect": "2.1.9", - "@vitest/mocker": "2.1.9", - "@vitest/pretty-format": "^2.1.9", - "@vitest/runner": "2.1.9", - "@vitest/snapshot": "2.1.9", - "@vitest/spy": "2.1.9", - "@vitest/utils": "2.1.9", - "chai": "^5.1.2", - "debug": "^4.3.7", - "expect-type": "^1.1.0", - "magic-string": "^0.30.12", - "pathe": "^1.1.2", - "std-env": "^3.8.0", + "version": "4.1.10", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/vitest/-/vitest-4.1.10.tgz", + "integrity": "sha1-fpKF7+JksRZwULejp/80eI4bevw=", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.10", + "@vitest/mocker": "4.1.10", + "@vitest/pretty-format": "4.1.10", + "@vitest/runner": "4.1.10", + "@vitest/snapshot": "4.1.10", + "@vitest/spy": "4.1.10", + "@vitest/utils": "4.1.10", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", "tinybench": "^2.9.0", - "tinyexec": "^0.3.1", - "tinypool": "^1.0.1", - "tinyrainbow": "^1.2.0", - "vite": "^5.0.0", - "vite-node": "2.1.9", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", "why-is-node-running": "^2.3.0" }, "bin": { "vitest": "vitest.mjs" }, "engines": { - "node": "^18.0.0 || >=20.0.0" + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" }, "funding": { "url": "https://opencollective.com/vitest" }, "peerDependencies": { "@edge-runtime/vm": "*", - "@types/node": "^18.0.0 || >=20.0.0", - "@vitest/browser": "2.1.9", - "@vitest/ui": "2.1.9", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.10", + "@vitest/browser-preview": "4.1.10", + "@vitest/browser-webdriverio": "4.1.10", + "@vitest/coverage-istanbul": "4.1.10", + "@vitest/coverage-v8": "4.1.10", + "@vitest/ui": "4.1.10", "happy-dom": "*", - "jsdom": "*" + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "@edge-runtime/vm": { "optional": true }, + "@opentelemetry/api": { + "optional": true + }, "@types/node": { "optional": true }, - "@vitest/browser": { + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { "optional": true }, "@vitest/ui": { @@ -3966,21 +9441,51 @@ }, "jsdom": { "optional": true + }, + "vite": { + "optional": false } } }, - "node_modules/vitest/node_modules/pathe": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", - "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "node_modules/vitest/node_modules/tinyexec": { + "version": "1.2.4", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/tinyexec/-/tinyexec-1.2.4.tgz", + "integrity": "sha1-rkW7Lt69qUxw9OqJfg8SQ+Rw23E=", "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/void-elements": { + "version": "3.1.0", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/void-elements/-/void-elements-3.1.0.tgz", + "integrity": "sha1-YU9/v42AHwu18GYfWy9XhXUOTwk=", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/w3c-keyname": { + "version": "2.2.8", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/w3c-keyname/-/w3c-keyname-2.2.8.tgz", + "integrity": "sha1-exfIxog9TouGrIq6edOeiA+IacU=", "license": "MIT" }, + "node_modules/web-namespaces": { + "version": "2.0.1", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/web-namespaces/-/web-namespaces-2.0.1.tgz", + "integrity": "sha1-EBD/fGUOzLJZLOvur5obJT/UBpI=", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/which": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/which/-/which-2.0.2.tgz", + "integrity": "sha1-fGqN0KY2oDJ+ELWckobu6T8/UbE=", "license": "ISC", "dependencies": { "isexe": "^2.0.0" @@ -3994,8 +9499,8 @@ }, "node_modules/why-is-node-running": { "version": "2.3.0", - "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", - "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha1-o/aalxB/SUs83Dvd3Yg6fWXOvwQ=", "dev": true, "license": "MIT", "dependencies": { @@ -4011,18 +9516,76 @@ }, "node_modules/word-wrap": { "version": "1.2.5", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", - "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha1-0sRcbdT7zmIaZvE2y+Mor9BBCzQ=", "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" } }, + "node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha1-lWgy3qlJQwbm0gnrhxZDu4c9fJg=", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "license": "ISC" + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha1-f0k00PfKjFb5UxSTndzS3ZHOHVU=", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "18.0.0", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/yargs/-/yargs-18.0.0.tgz", + "integrity": "sha1-bIQlmAYnOnRrCfV5CHtoo8LSW9E=", + "license": "MIT", + "dependencies": { + "cliui": "^9.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "string-width": "^7.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^22.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "node_modules/yargs-parser": { + "version": "22.0.0", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/yargs-parser/-/yargs-parser-22.0.0.tgz", + "integrity": "sha1-h7gglAUbBWdxc0bs0A/RSASzV8g=", + "license": "ISC", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha1-ApTrPe4FAo0x7hpfosVWpqrxChs=", "dev": true, "license": "MIT", "engines": { @@ -4031,6 +9594,307 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/zod/-/zod-3.25.76.tgz", + "integrity": "sha1-JoQcP2/SKmonYOfMtxkXl2hHHjQ=", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.25.2", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", + "integrity": "sha1-P6eZp7rdVUVBRy+2WEP9xGCy5ao=", + "license": "ISC", + "peerDependencies": { + "zod": "^3.25.28 || ^4" + } + }, + "node_modules/zrender": { + "version": "6.1.0", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/zrender/-/zrender-6.1.0.tgz", + "integrity": "sha1-w+8G5kJtXCiGW3GDA1aA1oXgATY=", + "license": "BSD-3-Clause", + "dependencies": { + "tslib": "2.3.0" + } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha1-yCfUsKy3b8PmhaTG7CkC1RBw6dc=", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "packages/flint-js": { + "name": "flint-chart", + "version": "0.4.1", + "license": "MIT", + "devDependencies": { + "@types/node": "^20.14.10", + "@typescript-eslint/eslint-plugin": "^8.16.0", + "@typescript-eslint/parser": "^8.16.0", + "eslint": "^9.15.0", + "eslint-plugin-unused-imports": "^4.4.1", + "prettier": "^3.3.0", + "rimraf": "^6.0.1", + "tsup": "^8.3.0", + "typescript": "^5.6.0", + "typescript-eslint": "^8.16.0", + "vitest": "^4.1.8" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "chart.js": "^4.0.0", + "echarts": "^5.0.0 || ^6.0.0", + "plotly.js": "^2.0.0 || ^3.0.0", + "vega": "^5.0.0 || ^6.0.0", + "vega-lite": "^5.0.0 || ^6.0.0" + }, + "peerDependenciesMeta": { + "chart.js": { + "optional": true + }, + "echarts": { + "optional": true + }, + "plotly.js": { + "optional": true + }, + "vega": { + "optional": true + }, + "vega-lite": { + "optional": true + } + } + }, + "packages/flint-mcp": { + "name": "flint-chart-mcp", + "version": "0.4.1", + "license": "MIT", + "dependencies": { + "@modelcontextprotocol/ext-apps": "^1.7.4", + "@modelcontextprotocol/sdk": "^1.29.0", + "@napi-rs/canvas": "^1.0.0", + "@resvg/resvg-js": "^2.6.2", + "chart.js": "^4.4.0", + "echarts": "^6.0.0", + "flint-chart": "^0.4.1", + "vega": "^6.0.0", + "vega-interpreter": "^2.2.1", + "vega-lite": "^6.0.0", + "zod": "^3.25.1" + }, + "bin": { + "flint-chart-mcp": "dist/cli.js" + }, + "devDependencies": { + "@types/node": "^20.14.10", + "@types/react": "^19.2.17", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.3", + "react": "^19.2.7", + "react-dom": "^19.2.7", + "rimraf": "^6.0.1", + "tsup": "^8.3.0", + "typescript": "^5.6.0", + "vite": "^8.1.0", + "vite-plugin-singlefile": "^2.3.3", + "vitest": "^4.1.8" + }, + "engines": { + "node": ">=18" + } + }, + "packages/flint-mcp/node_modules/@types/react": { + "version": "19.2.17", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@types/react/-/react-19.2.17.tgz", + "integrity": "sha1-3MrDZbqg8XNOwnD/S1HIlGXo3H8=", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "packages/flint-mcp/node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha1-weMF0VpSo+UI1U3Kdw0gLLY6vyw=", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "packages/flint-mcp/node_modules/react": { + "version": "19.2.7", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/react/-/react-19.2.7.tgz", + "integrity": "sha1-H0ehv8BvjsiFdSxvSvFDaan4Jgs=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "packages/flint-mcp/node_modules/react-dom": { + "version": "19.2.7", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/react-dom/-/react-dom-19.2.7.tgz", + "integrity": "sha1-BFDcmundv/du8ZZAHNi4x/tGbMw=", + "dev": true, + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.7" + } + }, + "packages/flint-mcp/node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha1-DE74LWfR5cHjWej8dtOofwRf5b0=", + "dev": true, + "license": "MIT" + }, + "site": { + "name": "flint-chart-site", + "version": "0.0.0", + "dependencies": { + "@codemirror/lang-json": "^6.0.1", + "@fontsource-variable/inter": "^5.2.8", + "@uiw/react-codemirror": "^4.23.0", + "chart.js": "^4.5.1", + "echarts": "^6.0.0", + "flint-chart": "*", + "i18next": "^26.3.6", + "katex": "^0.17.0", + "plotly.js-dist-min": "^2.35.2", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-i18next": "^17.0.10", + "react-markdown": "^10.1.0", + "react-router-dom": "^7.18.1", + "react-syntax-highlighter": "^16.1.1", + "rehype-katex": "^7.0.1", + "remark-gfm": "^4.0.1", + "remark-math": "^6.0.0", + "vega": "^6.0.0", + "vega-embed": "^7.1.0", + "vega-lite": "^6.4.1" + }, + "devDependencies": { + "@types/react": "^18.3.0", + "@types/react-dom": "^18.3.0", + "@types/react-syntax-highlighter": "^15.5.13", + "@vitejs/plugin-react-swc": "^3.7.0", + "typescript": "^5.6.0", + "vite": "^7.3.5" + } + }, + "site/node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha1-R9K/TO9tRwsi9YMbQg+JZOC/dV8=", + "dev": true, + "license": "MIT" + }, + "site/node_modules/@vitejs/plugin-react-swc": { + "version": "3.11.0", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@vitejs/plugin-react-swc/-/plugin-react-swc-3.11.0.tgz", + "integrity": "sha1-2CzDB9UwGXp3tQI4hgzzGYkP/Bc=", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "1.0.0-beta.27", + "@swc/core": "^1.12.11" + }, + "peerDependencies": { + "vite": "^4 || ^5 || ^6 || ^7" + } + }, + "site/node_modules/vite": { + "version": "7.3.5", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/vite/-/vite-7.3.5.tgz", + "integrity": "sha1-kMLQt7lKIk5+fc8i0pEv8LUpEWU=", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } } } } From ab844fbcb7d72f927f078f8a9ed8a85229bcdffa Mon Sep 17 00:00:00 2001 From: Chenglong Wang Date: Thu, 30 Jul 2026 16:44:51 -0700 Subject: [PATCH 037/164] fixes --- docs/reference-vegalite.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/reference-vegalite.md b/docs/reference-vegalite.md index 264a40fb..ce34ce3c 100644 --- a/docs/reference-vegalite.md +++ b/docs/reference-vegalite.md @@ -210,6 +210,10 @@ The **Availability** column shows whether a parameter is `always` available or ` | Parameter | Control | Domain | Default | Availability | Description | |---|---|---|---|---|---| | `bandwidth` | number | 0.05 – 2 (step 0.05) | `0` | always | Kernel-density bandwidth (0 = auto). | +| `showPoints` | toggle | on / off | `false` | always | Overlay point markers on the line. | +| `showMedian` | toggle | on / off | `false` | always | Median rule | +| `showContour` | toggle | on / off | `false` | always | Contour | +| `medianWidth` | number | 0.2 – 1 (step 0.05) | `0.6` | conditional | Median width | | `independentYAxis` | toggle | on / off | `false` | conditional | Use independent y-scales for facets. | ### ![](chart-icon-box-plot.svg) Boxplot @@ -284,6 +288,7 @@ _No template-specific parameters._ | Parameter | Control | Domain | Default | Availability | Description | |---|---|---|---|---|---| +| `interpolate` | choice | Default (linear) _(default)_, `linear` (Linear), `monotone` (Monotone (smooth)), `step` (Step), `step-before` (Step Before), `step-after` (Step After), `basis` (Basis (smooth)), `cardinal` (Cardinal), `catmull-rom` (Catmull-Rom) | — | always | Line or area interpolation method. | | `independentYAxis` | toggle | on / off | `false` | conditional | Use independent y-scales for facets. | | `logScale_x` | toggle | on / off | `false` | conditional | Use a log/symlog scale on the x-axis. | | `logScale_y` | toggle | on / off | `false` | conditional | Use a log/symlog scale on the y-axis. | @@ -296,6 +301,8 @@ _No template-specific parameters._ | Parameter | Control | Domain | Default | Availability | Description | |---|---|---|---|---|---| +| `showText` | toggle | on / off | `false` | always | Values | +| `showSeriesInLabel` | toggle | on / off | `false` | conditional | Name in label | | `independentYAxis` | toggle | on / off | `false` | conditional | Use independent y-scales for facets. | | `logScale_x` | toggle | on / off | `false` | conditional | Use a log/symlog scale on the x-axis. | | `logScale_y` | toggle | on / off | `false` | conditional | Use a log/symlog scale on the y-axis. | From 3576f961cacc2b23004c062c72b6c222cf0b7597 Mon Sep 17 00:00:00 2001 From: Chenglong Wang Date: Thu, 30 Jul 2026 23:28:18 -0700 Subject: [PATCH 038/164] theme: give each house a distinct layout envelope MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The R2 harness pinned every case to a 300-wide base size, so all six houses resolved to the same width and near-identical height — layout carried none of each house's identity even though their ink and type differed. compute-layout runs before the theme and is driven by chart_spec.baseSize, and the caller's baseSize won over the theme's, so a house had no way to express aspect ratio. Stop forcing baseSize in the harness (let the theme's compileDefaults.baseSize drive; flint falls back to the engine default) and raise the R2 canvas ceiling to 800x800. Give each preset a compileDefaults.baseSize matching its published proportions: economist wide-short 460x300 (~1.5:1), mckinsey wide slide 440x300, powerbi 16:9 480x280, datawrapper landscape 420x340, nyt near-square 380x340, nature small compact 300x250. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 33e4b998-4b58-4d7e-9cf8-c107b10e45c2 --- .../src/core/theme/presets/datawrapper.ts | 3 +++ .../flint-js/src/core/theme/presets/economist.ts | 3 +++ .../flint-js/src/core/theme/presets/mckinsey.ts | 3 +++ .../flint-js/src/core/theme/presets/nature.ts | 3 +++ packages/flint-js/src/core/theme/presets/nyt.ts | 3 +++ .../flint-js/src/core/theme/presets/powerbi.ts | 3 +++ site/src/playground/theme-lab-r2-data.ts | 15 ++++++++++----- 7 files changed, 28 insertions(+), 5 deletions(-) diff --git a/packages/flint-js/src/core/theme/presets/datawrapper.ts b/packages/flint-js/src/core/theme/presets/datawrapper.ts index 68ab6f5b..4dcdced8 100644 --- a/packages/flint-js/src/core/theme/presets/datawrapper.ts +++ b/packages/flint-js/src/core/theme/presets/datawrapper.ts @@ -159,6 +159,9 @@ export const datawrapper: ThemePreset = { "titleBlock": { "anchor": "start" } + }, + "compileDefaults": { + "baseSize": { "width": 420, "height": 340 } } }, }; diff --git a/packages/flint-js/src/core/theme/presets/economist.ts b/packages/flint-js/src/core/theme/presets/economist.ts index eb9e243e..c88c772b 100644 --- a/packages/flint-js/src/core/theme/presets/economist.ts +++ b/packages/flint-js/src/core/theme/presets/economist.ts @@ -158,6 +158,9 @@ export const economist: ThemePreset = { "anchor": "start" } }, + "compileDefaults": { + "baseSize": { "width": 460, "height": 300 } + }, "variants": [ { "when": { diff --git a/packages/flint-js/src/core/theme/presets/mckinsey.ts b/packages/flint-js/src/core/theme/presets/mckinsey.ts index 6746a7da..8c589b55 100644 --- a/packages/flint-js/src/core/theme/presets/mckinsey.ts +++ b/packages/flint-js/src/core/theme/presets/mckinsey.ts @@ -170,6 +170,9 @@ export const mckinsey: ThemePreset = { "anchor": "start" }, "bandStep": 80 + }, + "compileDefaults": { + "baseSize": { "width": 440, "height": 300 } } }, }; diff --git a/packages/flint-js/src/core/theme/presets/nature.ts b/packages/flint-js/src/core/theme/presets/nature.ts index 8bb60f41..5a439868 100644 --- a/packages/flint-js/src/core/theme/presets/nature.ts +++ b/packages/flint-js/src/core/theme/presets/nature.ts @@ -183,6 +183,9 @@ export const nature: ThemePreset = { }, "bandStep": 46 }, + "compileDefaults": { + "baseSize": { "width": 300, "height": 250 } + }, "chartDefaults": { "Boxplot": { "showPoints": true diff --git a/packages/flint-js/src/core/theme/presets/nyt.ts b/packages/flint-js/src/core/theme/presets/nyt.ts index 579ab69a..12b7f1bb 100644 --- a/packages/flint-js/src/core/theme/presets/nyt.ts +++ b/packages/flint-js/src/core/theme/presets/nyt.ts @@ -166,6 +166,9 @@ export const nyt: ThemePreset = { "anchor": "start" } }, + "compileDefaults": { + "baseSize": { "width": 380, "height": 340 } + }, "chartDefaults": { "Line Chart": { "showPoints": true diff --git a/packages/flint-js/src/core/theme/presets/powerbi.ts b/packages/flint-js/src/core/theme/presets/powerbi.ts index 7274f13b..f55c18d0 100644 --- a/packages/flint-js/src/core/theme/presets/powerbi.ts +++ b/packages/flint-js/src/core/theme/presets/powerbi.ts @@ -182,6 +182,9 @@ export const powerbi: ThemePreset = { "titleBlock": { "anchor": "start" } + }, + "compileDefaults": { + "baseSize": { "width": 480, "height": 280 } } }, }; diff --git a/site/src/playground/theme-lab-r2-data.ts b/site/src/playground/theme-lab-r2-data.ts index 3ae7f16f..0828d5d4 100644 --- a/site/src/playground/theme-lab-r2-data.ts +++ b/site/src/playground/theme-lab-r2-data.ts @@ -159,8 +159,10 @@ export const R2_CASES: R2Case[] = [ /** The base canvas every R2 case is designed at, so sheets are comparable. */ export const R2_BASE_SIZE = { width: 300, height: 300 }; -/** The ceiling a case may stretch to when its data needs more room (1.5×). */ -export const R2_CANVAS_SIZE = { width: 450, height: 450 }; +/** A generous square stretch ceiling. Each house starts from its own + * `compileDefaults.baseSize` (aspect ratio + footprint) and may grow up to + * this when the data needs the room, so the shape is the house's, not ours. */ +export const R2_CANVAS_SIZE = { width: 800, height: 800 }; const CASE_CACHE = new Map(); @@ -184,9 +186,12 @@ export function r2TestCase(c: R2Case): TestCase { export function r2Input(c: R2Case): any { const t = r2TestCase(c); const input = testCaseToAssemblyInput(t, R2_BASE_SIZE); - // A design size of 300² with a 450² ceiling: the house lays out at the - // base and is allowed to stretch each dimension up to 1.5× when the data - // (many bands, a long legend) needs the room. + // Let each house's own `compileDefaults.baseSize` drive its aspect ratio + // and footprint: dropping the caller's baseSize lets the theme's win + // (flint, with no theme, falls back to flint's neutral default). A single + // generous square ceiling lets every house stretch when the data needs it + // without dictating the shape it starts from. + delete input.chart_spec.baseSize; input.chart_spec.canvasSize = R2_CANVAS_SIZE; input.chart_spec.title = c.title; if (c.subtitle) input.chart_spec.subtitle = c.subtitle; From e9e907b11f15d5bf76bdd268abe5a2a180376283 Mon Sep 17 00:00:00 2001 From: Chenglong Wang Date: Thu, 30 Jul 2026 23:28:32 -0700 Subject: [PATCH 039/164] playground: add theme reference-examples tab MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Hand-authored, faithful Vega-Lite recreations of one signature chart per house (Economist pale-blue line with red emphasis + right y-axis, NYT direct-labelled lines, Nature small framed grouped bars with error bars + panel letter, McKinsey navy value-labelled columns, Datawrapper single-blue ranked bars, Power BI dark 16:9 dashboard tile). Colours, fonts, gridline treatment and aspect ratios are lifted from each house's published style; each card lists the design principles it teaches. These are the visual targets the house ThemeSpecs are tuned towards — read the gap against the R2 grid. Cards render lazily on scroll. New route /playground/theme-lab-reference. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 33e4b998-4b58-4d7e-9cf8-c107b10e45c2 --- site/src/main.tsx | 2 + site/src/playground/PlaygroundShell.tsx | 1 + site/src/playground/ThemeLabReference.tsx | 181 ++++++ .../playground/theme-lab-reference-data.ts | 614 ++++++++++++++++++ 4 files changed, 798 insertions(+) create mode 100644 site/src/playground/ThemeLabReference.tsx create mode 100644 site/src/playground/theme-lab-reference-data.ts diff --git a/site/src/main.tsx b/site/src/main.tsx index c74baa4a..fe850376 100644 --- a/site/src/main.tsx +++ b/site/src/main.tsx @@ -18,6 +18,7 @@ import { DemoWall } from './playground/DemoWall'; import { ThemeLab } from './playground/ThemeLab'; import { ThemeLabR2 } from './playground/ThemeLabR2'; import { ThemeLabGaps } from './playground/ThemeLabGaps'; +import { ThemeLabReference } from './playground/ThemeLabReference'; import { FullTestCases } from './playground/FullTestCases'; import { LocaleProvider, useLocale } from './i18n/LocaleContext'; import type { Locale } from './i18n/locales'; @@ -58,6 +59,7 @@ function AppRoutes({ locale }: { locale: Locale }) { } /> } /> } /> + } /> } /> {/* Tutorials merged into Documentation as the "Quick start" group. */} diff --git a/site/src/playground/PlaygroundShell.tsx b/site/src/playground/PlaygroundShell.tsx index 77b33e05..b7ff510e 100644 --- a/site/src/playground/PlaygroundShell.tsx +++ b/site/src/playground/PlaygroundShell.tsx @@ -10,6 +10,7 @@ const pages = [ { to: 'theme-labs', label: 'Theme lab' }, { to: 'theme-lab-r2', label: 'Theme lab R2' }, { to: 'theme-lab-gaps', label: 'Theme lab gaps' }, + { to: 'theme-lab-reference', label: 'Theme references' }, { to: 'full-test-cases', label: 'Full test cases' }, ]; diff --git a/site/src/playground/ThemeLabReference.tsx b/site/src/playground/ThemeLabReference.tsx new file mode 100644 index 00000000..ccdc6272 --- /dev/null +++ b/site/src/playground/ThemeLabReference.tsx @@ -0,0 +1,181 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** + * Theme lab — reference examples. + * + * Hand-authored, faithful recreations of one signature chart per design house + * (see `theme-lab-reference-data.ts`). These are the visual targets the house + * ThemeSpecs are tuned towards; each card lists the design principles it is + * meant to teach so the gap to Flint's compiled output can be read off + * directly. Cards render lazily on scroll to keep the page responsive. + */ + +import { useEffect, useMemo, useRef, useState, type ReactNode } from 'react'; +import { VegaLiteView } from '../components/VegaLiteView'; +import { siteTheme } from '../shared/theme'; +import { + REFERENCE_CASES, + REFERENCE_HOUSE_ORDER, + REFERENCE_HOUSE_LABEL, + type ReferenceCase, + type ReferenceHouse, +} from './theme-lab-reference-data'; + +function Pill({ children }: { children: ReactNode }) { + return ( + + {children} + + ); +} + +function ReferenceCard({ c }: { c: ReferenceCase }) { + const ref = useRef(null); + const [visible, setVisible] = useState(false); + + useEffect(() => { + const el = ref.current; + if (!el) return; + const observer = new IntersectionObserver( + ([entry]) => { + if (entry.isIntersecting) { + setVisible(true); + observer.disconnect(); + } + }, + { rootMargin: '300px' }, + ); + observer.observe(el); + return () => observer.disconnect(); + }, []); + + const spec = useMemo(() => (visible ? c.spec : null), [visible, c]); + + return ( +
+
+ {c.id} · {c.title} +
+
+ {c.tab ? ( +
+ ) : null} +
+ {spec ? ( + + ) : ( + + )} +
+
+
+ {c.sourceNote} +
+
    + {c.principles.map((p) => ( +
  • {p}
  • + ))} +
+
+ ); +} + +export function ThemeLabReference() { + const [house, setHouse] = useState('all'); + const cases = + house === 'all' ? REFERENCE_CASES : REFERENCE_CASES.filter((c) => c.house === house); + + return ( +
+
+

+ Theme lab · reference examples +

+

+ Hand-authored recreations of a signature chart from each house, faithful to its + published colour, type, gridline and aspect-ratio conventions. These are the + targets the house ThemeSpecs are tuned towards — read the gap against the R2 grid. +

+
+ + + +
+ {cases.map((c) => ( + + ))} +
+
+ ); +} diff --git a/site/src/playground/theme-lab-reference-data.ts b/site/src/playground/theme-lab-reference-data.ts new file mode 100644 index 00000000..0f5416f0 --- /dev/null +++ b/site/src/playground/theme-lab-reference-data.ts @@ -0,0 +1,614 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** + * Theme lab — reference examples. + * + * Hand-authored Vega-Lite specs that recreate one or two *signature* charts + * from each design house, as faithfully as a from-scratch spec can. These are + * not produced by Flint's compiler; they are the target we tune the house + * ThemeSpecs *towards*. Colours, fonts, gridline treatment, axis placement, + * canvas aspect ratio and direct-labelling are all lifted from the house's own + * published style (see `sourceNote` on each entry). + * + * Use: put a reference beside the same-family cell on the R2 grid and read off + * the gap — aspect ratio, band step, label placement, ink — then push the + * ThemeSpec to close it. The `principles` array records what each reference is + * meant to teach. + */ + +export type ReferenceHouse = + | 'nyt' + | 'economist' + | 'nature' + | 'mckinsey' + | 'datawrapper' + | 'powerbi'; + +export interface ReferenceCase { + id: string; + house: ReferenceHouse; + houseLabel: string; + title: string; + /** Where the visual language comes from. */ + sourceNote: string; + /** Design parameters this reference is meant to demonstrate/transfer. */ + principles: string[]; + /** A coloured marker strip drawn above the chart (e.g. Economist red tab). */ + tab?: string; + /** Outer tile background, so a dark house reads correctly behind the chart. */ + tile?: string; + width: number; + height: number; + spec: any; +} + +export const REFERENCE_HOUSE_ORDER: ReferenceHouse[] = [ + 'economist', + 'nyt', + 'nature', + 'mckinsey', + 'datawrapper', + 'powerbi', +]; + +export const REFERENCE_HOUSE_LABEL: Record = { + economist: 'The Economist', + nyt: 'New York Times', + nature: 'Nature', + mckinsey: 'McKinsey', + datawrapper: 'Datawrapper', + powerbi: 'Power BI', +}; + +// --------------------------------------------------------------------------- +// Shared tiny datasets +// --------------------------------------------------------------------------- + +const YEARS = [2015, 2016, 2017, 2018, 2019, 2020, 2021, 2022, 2023]; + +function series(name: string, vals: number[]) { + return YEARS.map((year, i) => ({ year, series: name, value: vals[i] })); +} + +// --------------------------------------------------------------------------- +// The Economist — time-series line, pale blue panel, red top tab, right y-axis +// --------------------------------------------------------------------------- + +const economistLine: ReferenceCase = { + id: 'economist-line', + house: 'economist', + houseLabel: REFERENCE_HOUSE_LABEL.economist, + title: 'GDP per person', + sourceNote: + 'economist.com Graphic detail + ggthemes theme_economist: panel #d5e4eb, white horizontal gridlines, y-axis on the right with no domain line, one red series for emphasis.', + principles: [ + 'Wide-short canvas ~1.6:1 (small multiples run ~290×207px).', + 'Panel fill #d5e4eb; gridlines are WHITE and horizontal-only; no y domain/ticks.', + 'y-axis sits on the RIGHT; x-axis keeps a black baseline.', + 'Red (#e3120b) reserved for the one series that matters; everything else muted blue.', + 'Bold sans headline flush-left over a red tab; deck in grey.', + ], + tab: '#e3120b', + width: 380, + height: 236, + spec: { + config: { view: { stroke: null } }, + background: '#d5e4eb', + width: 320, + height: 176, + font: "'Helvetica Neue', Helvetica, Arial, sans-serif", + title: { + text: 'Pulling ahead', + subtitle: 'GDP per person, $’000 at PPP', + anchor: 'start', + fontSize: 15, + fontWeight: 700, + color: '#121317', + subtitleFontSize: 11, + subtitleColor: '#54585a', + offset: 12, + }, + data: { + values: [ + ...series('United States', [55, 57, 59, 62, 65, 63, 70, 76, 80]), + ...series('Euro area', [39, 40, 41, 43, 45, 43, 47, 50, 52]), + ], + }, + encoding: { + x: { + field: 'year', + type: 'ordinal', + axis: { + labelAngle: 0, + domainColor: '#121317', + domainWidth: 1, + tickColor: '#121317', + grid: false, + labelColor: '#121317', + labelFontSize: 11, + title: null, + values: [2015, 2017, 2019, 2021, 2023], + }, + }, + y: { + field: 'value', + type: 'quantitative', + scale: { domain: [30, 85] }, + axis: { + orient: 'right', + grid: true, + gridColor: '#ffffff', + gridWidth: 1.4, + domain: false, + ticks: false, + labelColor: '#121317', + labelFontSize: 11, + title: null, + tickCount: 5, + }, + }, + color: { + field: 'series', + type: 'nominal', + scale: { + domain: ['United States', 'Euro area'], + range: ['#e3120b', '#006ba2'], + }, + legend: null, + }, + }, + layer: [ + { mark: { type: 'line', strokeWidth: 3, interpolate: 'monotone' } }, + { + transform: [ + { filter: 'datum.year === 2023' }, + ], + mark: { type: 'text', align: 'right', dx: -4, dy: -8, fontSize: 11, fontWeight: 700 }, + encoding: { text: { field: 'series' } }, + }, + ], + }, +}; + +// --------------------------------------------------------------------------- +// New York Times — multi-line, direct end labels, muted ink, subtle grid +// --------------------------------------------------------------------------- + +const nytLine: ReferenceCase = { + id: 'nyt-line', + house: 'nyt', + houseLabel: REFERENCE_HOUSE_LABEL.nyt, + title: 'Direct-labelled trend lines', + sourceNote: + 'NYT graphics desk house style: near-square mobile-first canvas, no colour legend — series named at the line end, faint horizontal grid, serif deck over a sans body.', + principles: [ + 'Near-square canvas (~1.1:1) built mobile-first.', + 'No legend: label each line at its right end; reserve red for the lead series.', + 'Horizontal grid only, very light (#e4e4e4); drop the y domain + ticks.', + 'Extra right padding so the end labels have room to breathe.', + ], + width: 340, + height: 320, + spec: { + config: { view: { stroke: null } }, + background: '#ffffff', + width: 220, + height: 232, + padding: { right: 84, left: 4, top: 4, bottom: 4 }, + font: "'Helvetica Neue', Helvetica, Arial, sans-serif", + title: { + text: 'Streaming pulls even', + subtitle: 'Share of viewing hours, %', + anchor: 'start', + fontSize: 16, + fontWeight: 700, + color: '#121212', + font: 'Georgia, "Times New Roman", serif', + subtitleFont: "'Helvetica Neue', Helvetica, Arial, sans-serif", + subtitleFontSize: 12, + subtitleColor: '#6b6b6b', + offset: 12, + }, + data: { + values: [ + ...series('Streaming', [12, 16, 20, 25, 29, 34, 38, 40, 41]), + ...series('Cable', [42, 40, 38, 36, 34, 31, 28, 25, 23]), + ...series('Broadcast', [30, 28, 26, 24, 22, 21, 20, 19, 18]), + ], + }, + encoding: { + x: { + field: 'year', + type: 'quantitative', + scale: { domain: [2015, 2023], nice: false }, + axis: { + format: 'd', + grid: false, + domainColor: '#c9c9c9', + tickColor: '#c9c9c9', + labelColor: '#6b6b6b', + labelFontSize: 11, + title: null, + values: [2015, 2019, 2023], + }, + }, + y: { + field: 'value', + type: 'quantitative', + scale: { domain: [0, 45] }, + axis: { + grid: true, + gridColor: '#e4e4e4', + domain: false, + ticks: false, + labelColor: '#6b6b6b', + labelFontSize: 11, + title: null, + tickCount: 4, + }, + }, + color: { + field: 'series', + type: 'nominal', + scale: { + domain: ['Streaming', 'Cable', 'Broadcast'], + range: ['#c2352b', '#2f6b9a', '#8a8a8a'], + }, + legend: null, + }, + }, + layer: [ + { mark: { type: 'line', strokeWidth: 2.5 } }, + { + transform: [{ filter: 'datum.year === 2023' }], + mark: { type: 'text', align: 'left', dx: 6, fontSize: 11, fontWeight: 700 }, + encoding: { text: { field: 'series' } }, + }, + ], + }, +}; + +// --------------------------------------------------------------------------- +// Nature — small grouped bars with error bars, black frame, panel label +// --------------------------------------------------------------------------- + +const natureBars: ReferenceCase = { + id: 'nature-grouped', + house: 'nature', + houseLabel: REFERENCE_HOUSE_LABEL.nature, + title: 'Grouped bars with error bars', + sourceNote: + 'Nature figure conventions: single-column (~89 mm) compact panel, 6–7pt Arial, thin black L+B frame, no grid, Okabe–Ito colourblind-safe palette, bold panel letter, s.e.m. whiskers.', + principles: [ + 'Small, dense single-column panel — do NOT inflate the canvas.', + 'Tiny type (labels 7pt); thin black axis frame on left + bottom, no gridlines.', + 'Colourblind-safe categorical palette (Okabe–Ito).', + 'Bold lowercase panel letter top-left; error bars on every bar.', + ], + tab: undefined, + width: 260, + height: 220, + spec: { + config: { view: { stroke: null } }, + background: '#ffffff', + width: 190, + height: 150, + font: 'Arial, Helvetica, sans-serif', + title: { + text: 'a', + anchor: 'start', + fontSize: 15, + fontWeight: 700, + color: '#000000', + offset: 6, + }, + data: { + values: [ + { group: 'WT', cond: 'Control', value: 3.1, se: 0.3 }, + { group: 'WT', cond: 'Treated', value: 5.4, se: 0.4 }, + { group: 'KO', cond: 'Control', value: 2.7, se: 0.25 }, + { group: 'KO', cond: 'Treated', value: 3.2, se: 0.35 }, + ], + }, + encoding: { + x: { + field: 'group', + type: 'nominal', + axis: { + domainColor: '#000000', + domainWidth: 1, + tickColor: '#000000', + labelColor: '#000000', + labelFontSize: 8, + labelAngle: 0, + title: null, + grid: false, + }, + }, + y: { + field: 'value', + type: 'quantitative', + scale: { domain: [0, 6] }, + axis: { + domainColor: '#000000', + domainWidth: 1, + tickColor: '#000000', + grid: false, + labelColor: '#000000', + labelFontSize: 8, + title: 'mRNA (a.u.)', + titleColor: '#000000', + titleFontSize: 8, + titleFontWeight: 400, + tickCount: 4, + }, + }, + xOffset: { field: 'cond' }, + color: { + field: 'cond', + type: 'nominal', + scale: { domain: ['Control', 'Treated'], range: ['#0072b2', '#e69f00'] }, + legend: { + orient: 'top-right', + title: null, + labelFontSize: 8, + symbolSize: 40, + offset: 2, + }, + }, + }, + layer: [ + { mark: { type: 'bar' } }, + { + mark: { type: 'errorbar', ticks: true, color: '#000000' }, + encoding: { + y: { field: 'value', type: 'quantitative' }, + yError: { field: 'se' }, + }, + }, + ], + }, +}; + +// --------------------------------------------------------------------------- +// McKinsey — column chart, navy bars, direct value labels, no y-axis +// --------------------------------------------------------------------------- + +const mckinseyColumns: ReferenceCase = { + id: 'mckinsey-columns', + house: 'mckinsey', + houseLabel: REFERENCE_HOUSE_LABEL.mckinsey, + title: 'Value-labelled columns', + sourceNote: + 'McKinsey exhibit style: landscape slide proportions, thick navy (#051c2c) bars, value printed on top of each bar, no y-axis and no gridlines, one bar picked out in the blue accent, action-title headline.', + principles: [ + 'Wide slide canvas (~1.5:1); thick bars with generous whitespace.', + 'No y-axis, no gridlines — the number lives on top of the bar.', + 'Deep navy default; single accent (#2251ff) to highlight one column.', + 'Left-aligned action title, small grey source line.', + ], + width: 420, + height: 280, + spec: { + config: { view: { stroke: null } }, + background: '#ffffff', + width: 360, + height: 180, + font: "'Helvetica Neue', Helvetica, Arial, sans-serif", + title: { + text: 'Digital revenue nearly doubled in three years', + subtitle: 'Revenue, $bn', + anchor: 'start', + fontSize: 15, + fontWeight: 700, + color: '#051c2c', + subtitleFontSize: 11, + subtitleColor: '#5a6872', + offset: 14, + }, + data: { + values: [ + { q: '2020', value: 4.2 }, + { q: '2021', value: 5.1 }, + { q: '2022', value: 6.4 }, + { q: '2023', value: 8.0 }, + ], + }, + encoding: { + x: { + field: 'q', + type: 'nominal', + axis: { + domainColor: '#051c2c', + domainWidth: 1, + ticks: false, + labelColor: '#051c2c', + labelFontSize: 12, + labelPadding: 6, + labelAngle: 0, + title: null, + grid: false, + }, + }, + y: { + field: 'value', + type: 'quantitative', + axis: null, + }, + color: { + condition: { test: "datum.q === '2023'", value: '#2251ff' }, + value: '#051c2c', + }, + }, + layer: [ + { mark: { type: 'bar', size: 46 } }, + { + mark: { type: 'text', dy: -8, fontSize: 12, fontWeight: 700, color: '#051c2c' }, + encoding: { text: { field: 'value', format: '.1f' } }, + }, + ], + }, +}; + +// --------------------------------------------------------------------------- +// Datawrapper — horizontal bars, single blue, light grid, title/deck/source +// --------------------------------------------------------------------------- + +const datawrapperBars: ReferenceCase = { + id: 'datawrapper-bars', + house: 'datawrapper', + houseLabel: REFERENCE_HOUSE_LABEL.datawrapper, + title: 'Ranked horizontal bars', + sourceNote: + 'Datawrapper default theme: Roboto, single blue (#18a1cd), value axis with light #dcdcdc gridlines, bold #333 title over a grey deck, bars sorted, footer source line.', + principles: [ + 'Moderate landscape canvas (~1.25:1); bar height row-driven.', + 'Single brand blue #18a1cd; value gridlines light grey, category axis bare.', + 'Roboto/Arial; bold dark title, grey deck, small grey source footer.', + 'Bars sorted by value; direct value labels optional.', + ], + width: 380, + height: 300, + spec: { + config: { view: { stroke: null } }, + background: '#ffffff', + width: 210, + height: 190, + font: "Roboto, 'Helvetica Neue', Arial, sans-serif", + title: { + text: 'Where the visits came from', + subtitle: 'Share of sessions, %', + anchor: 'start', + fontSize: 15, + fontWeight: 700, + color: '#333333', + subtitleFontSize: 11, + subtitleColor: '#666666', + offset: 12, + }, + data: { + values: [ + { channel: 'Search', value: 41 }, + { channel: 'Direct', value: 27 }, + { channel: 'Social', value: 18 }, + { channel: 'Email', value: 9 }, + { channel: 'Referral', value: 5 }, + ], + }, + encoding: { + y: { + field: 'channel', + type: 'nominal', + sort: '-x', + axis: { + domain: false, + ticks: false, + labelColor: '#333333', + labelFontSize: 12, + title: null, + grid: false, + }, + }, + x: { + field: 'value', + type: 'quantitative', + axis: { + grid: true, + gridColor: '#dcdcdc', + domain: false, + ticks: false, + labelColor: '#666666', + labelFontSize: 10, + title: null, + tickCount: 4, + }, + }, + }, + mark: { type: 'bar', color: '#18a1cd', height: 20 }, + }, +}; + +// --------------------------------------------------------------------------- +// Power BI — dark dashboard tile, blue columns, light grid, Segoe UI +// --------------------------------------------------------------------------- + +const powerbiColumns: ReferenceCase = { + id: 'powerbi-columns', + house: 'powerbi', + houseLabel: REFERENCE_HOUSE_LABEL.powerbi, + title: 'Dashboard tile columns', + sourceNote: + 'Power BI default (dark) theme: 16:9 tile on a near-black canvas (#1b1a19), #118dff data colour, low-contrast #3b3a39 gridlines, Segoe UI, small tile title top-left.', + principles: [ + 'Wide 16:9 dashboard-tile canvas (~1.7:1).', + 'Dark panel #1b1a19; light text; single blue #118dff; faint grid #3b3a39.', + 'Segoe UI; compact tile title, no deck.', + ], + tile: '#1b1a19', + width: 440, + height: 260, + spec: { + config: { view: { stroke: null } }, + background: '#1b1a19', + width: 380, + height: 176, + font: "'Segoe UI', 'Helvetica Neue', Arial, sans-serif", + title: { + text: 'Sales by region', + anchor: 'start', + fontSize: 14, + fontWeight: 600, + color: '#f3f2f1', + offset: 10, + }, + data: { + values: [ + { region: 'North', value: 128 }, + { region: 'South', value: 94 }, + { region: 'East', value: 112 }, + { region: 'West', value: 76 }, + { region: 'Central', value: 88 }, + ], + }, + encoding: { + x: { + field: 'region', + type: 'nominal', + axis: { + domainColor: '#3b3a39', + ticks: false, + labelColor: '#c8c6c4', + labelFontSize: 11, + title: null, + grid: false, + labelAngle: 0, + }, + }, + y: { + field: 'value', + type: 'quantitative', + axis: { + grid: true, + gridColor: '#3b3a39', + domain: false, + ticks: false, + labelColor: '#a19f9d', + labelFontSize: 10, + title: null, + tickCount: 4, + }, + }, + }, + mark: { type: 'bar', color: '#118dff', size: 40 }, + }, +}; + +export const REFERENCE_CASES: ReferenceCase[] = [ + economistLine, + nytLine, + natureBars, + mckinseyColumns, + datawrapperBars, + powerbiColumns, +]; From 1a336ce3a65673207441eebbadcac2f3a644aeac Mon Sep 17 00:00:00 2001 From: Chenglong Wang Date: Thu, 30 Jul 2026 23:39:24 -0700 Subject: [PATCH 040/164] theme(economist): right-hand measure axis is the house default MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Economist's most recognisable layout signature — the value axis on the right — was only applied to bar charts (markChannel: length) and part-to-whole areas, so its flagship time-series line and its scatterplots kept the axis on the left, reading as generic. ggthemes theme_economist documents the rule as universal ("the y axis should be displayed on the right hand side"). Make placement: opposite the base measure-axis default and keep the one measured exception (a non-part-to-whole range/area band, seattle-range, stays left) as a targeted variant. In grounding, scope the opposite→top flip to the NON-index horizontal axis: a scatter's x-axis is its reading axis and must stay at the bottom (only y moves to the right), while a horizontal bar's value axis — the non-index x — still moves to the top as before. Lines and vertical bars are unaffected (their x is categorical, drawn at the bottom). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 33e4b998-4b58-4d7e-9cf8-c107b10e45c2 --- packages/flint-js/src/core/theme/ground.ts | 2 +- .../src/core/theme/presets/economist.ts | 24 ++++--------------- 2 files changed, 6 insertions(+), 20 deletions(-) diff --git a/packages/flint-js/src/core/theme/ground.ts b/packages/flint-js/src/core/theme/ground.ts index 2d6ea192..5e6d337e 100644 --- a/packages/flint-js/src/core/theme/ground.ts +++ b/packages/flint-js/src/core/theme/ground.ts @@ -612,7 +612,7 @@ export function groundTheme(themeIn: ThemeSpec, ctx: GroundingContext): DesignDe const spec = role === 'measure' ? structure.axis?.measure : structure.axis?.categorical; const opposite = spec?.placement === 'opposite'; const orient: ResolvedAxis['orient'] = channel === 'x' - ? (opposite ? 'top' : 'bottom') + ? (opposite && channel !== indexChannel ? 'top' : 'bottom') : (opposite ? 'right' : 'left'); // The axis a reader indexes the chart *by* is not always the discrete diff --git a/packages/flint-js/src/core/theme/presets/economist.ts b/packages/flint-js/src/core/theme/presets/economist.ts index c88c772b..8e0f7bc0 100644 --- a/packages/flint-js/src/core/theme/presets/economist.ts +++ b/packages/flint-js/src/core/theme/presets/economist.ts @@ -93,7 +93,7 @@ export const economist: ThemePreset = { "measure": { "line": "omit", "ticks": "omit", - "placement": "default" + "placement": "opposite" } }, "grid": { @@ -164,33 +164,19 @@ export const economist: ThemePreset = { "variants": [ { "when": { - "markChannel": "length" + "markChannel": "area", + "isPartToWhole": false }, "then": { "structure": { "axis": { "measure": { - "placement": "opposite" + "placement": "default" } } } }, - "because": "Measured: big-mac, causes-death and state-jobless all carry the measure axis opposite (3 of 3 bar charts). On a banded chart the far edge is where a reader enters." - }, - { - "when": { - "isPartToWhole": true - }, - "then": { - "structure": { - "axis": { - "measure": { - "placement": "opposite" - } - } - } - }, - "because": "Measured: electricity-mix-area puts y on the right, seattle-range does not. Both are area marks, so markChannel cannot separate them; isPartToWhole can. On a pie the policy is inert." + "because": "Right-hand measure axis is the house default (ggthemes theme_economist; measured opposite on 3/3 bar charts and the electricity-mix part-to-whole area). The one measured exception is a non-part-to-whole range/area band (seattle-range), which keeps y on the left." } ], "chartDefaults": { From 3e24164ee1ddf81783ba553e41ce2861f7dadb78 Mon Sep 17 00:00:00 2001 From: Chenglong Wang Date: Fri, 31 Jul 2026 00:08:38 -0700 Subject: [PATCH 041/164] Revert "playground: add theme reference-examples tab" Remove the theme reference-examples gallery and its route/nav wiring; the hand-authored recreations were low quality. This reverts commit e9e907b. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 33e4b998-4b58-4d7e-9cf8-c107b10e45c2 --- site/src/main.tsx | 2 - site/src/playground/PlaygroundShell.tsx | 1 - site/src/playground/ThemeLabReference.tsx | 181 ------ .../playground/theme-lab-reference-data.ts | 614 ------------------ 4 files changed, 798 deletions(-) delete mode 100644 site/src/playground/ThemeLabReference.tsx delete mode 100644 site/src/playground/theme-lab-reference-data.ts diff --git a/site/src/main.tsx b/site/src/main.tsx index fe850376..c74baa4a 100644 --- a/site/src/main.tsx +++ b/site/src/main.tsx @@ -18,7 +18,6 @@ import { DemoWall } from './playground/DemoWall'; import { ThemeLab } from './playground/ThemeLab'; import { ThemeLabR2 } from './playground/ThemeLabR2'; import { ThemeLabGaps } from './playground/ThemeLabGaps'; -import { ThemeLabReference } from './playground/ThemeLabReference'; import { FullTestCases } from './playground/FullTestCases'; import { LocaleProvider, useLocale } from './i18n/LocaleContext'; import type { Locale } from './i18n/locales'; @@ -59,7 +58,6 @@ function AppRoutes({ locale }: { locale: Locale }) { } /> } /> } /> - } /> } /> {/* Tutorials merged into Documentation as the "Quick start" group. */} diff --git a/site/src/playground/PlaygroundShell.tsx b/site/src/playground/PlaygroundShell.tsx index b7ff510e..77b33e05 100644 --- a/site/src/playground/PlaygroundShell.tsx +++ b/site/src/playground/PlaygroundShell.tsx @@ -10,7 +10,6 @@ const pages = [ { to: 'theme-labs', label: 'Theme lab' }, { to: 'theme-lab-r2', label: 'Theme lab R2' }, { to: 'theme-lab-gaps', label: 'Theme lab gaps' }, - { to: 'theme-lab-reference', label: 'Theme references' }, { to: 'full-test-cases', label: 'Full test cases' }, ]; diff --git a/site/src/playground/ThemeLabReference.tsx b/site/src/playground/ThemeLabReference.tsx deleted file mode 100644 index ccdc6272..00000000 --- a/site/src/playground/ThemeLabReference.tsx +++ /dev/null @@ -1,181 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -/** - * Theme lab — reference examples. - * - * Hand-authored, faithful recreations of one signature chart per design house - * (see `theme-lab-reference-data.ts`). These are the visual targets the house - * ThemeSpecs are tuned towards; each card lists the design principles it is - * meant to teach so the gap to Flint's compiled output can be read off - * directly. Cards render lazily on scroll to keep the page responsive. - */ - -import { useEffect, useMemo, useRef, useState, type ReactNode } from 'react'; -import { VegaLiteView } from '../components/VegaLiteView'; -import { siteTheme } from '../shared/theme'; -import { - REFERENCE_CASES, - REFERENCE_HOUSE_ORDER, - REFERENCE_HOUSE_LABEL, - type ReferenceCase, - type ReferenceHouse, -} from './theme-lab-reference-data'; - -function Pill({ children }: { children: ReactNode }) { - return ( - - {children} - - ); -} - -function ReferenceCard({ c }: { c: ReferenceCase }) { - const ref = useRef(null); - const [visible, setVisible] = useState(false); - - useEffect(() => { - const el = ref.current; - if (!el) return; - const observer = new IntersectionObserver( - ([entry]) => { - if (entry.isIntersecting) { - setVisible(true); - observer.disconnect(); - } - }, - { rootMargin: '300px' }, - ); - observer.observe(el); - return () => observer.disconnect(); - }, []); - - const spec = useMemo(() => (visible ? c.spec : null), [visible, c]); - - return ( -
-
- {c.id} · {c.title} -
-
- {c.tab ? ( -
- ) : null} -
- {spec ? ( - - ) : ( - - )} -
-
-
- {c.sourceNote} -
-
    - {c.principles.map((p) => ( -
  • {p}
  • - ))} -
-
- ); -} - -export function ThemeLabReference() { - const [house, setHouse] = useState('all'); - const cases = - house === 'all' ? REFERENCE_CASES : REFERENCE_CASES.filter((c) => c.house === house); - - return ( -
-
-

- Theme lab · reference examples -

-

- Hand-authored recreations of a signature chart from each house, faithful to its - published colour, type, gridline and aspect-ratio conventions. These are the - targets the house ThemeSpecs are tuned towards — read the gap against the R2 grid. -

-
- - - -
- {cases.map((c) => ( - - ))} -
-
- ); -} diff --git a/site/src/playground/theme-lab-reference-data.ts b/site/src/playground/theme-lab-reference-data.ts deleted file mode 100644 index 0f5416f0..00000000 --- a/site/src/playground/theme-lab-reference-data.ts +++ /dev/null @@ -1,614 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -/** - * Theme lab — reference examples. - * - * Hand-authored Vega-Lite specs that recreate one or two *signature* charts - * from each design house, as faithfully as a from-scratch spec can. These are - * not produced by Flint's compiler; they are the target we tune the house - * ThemeSpecs *towards*. Colours, fonts, gridline treatment, axis placement, - * canvas aspect ratio and direct-labelling are all lifted from the house's own - * published style (see `sourceNote` on each entry). - * - * Use: put a reference beside the same-family cell on the R2 grid and read off - * the gap — aspect ratio, band step, label placement, ink — then push the - * ThemeSpec to close it. The `principles` array records what each reference is - * meant to teach. - */ - -export type ReferenceHouse = - | 'nyt' - | 'economist' - | 'nature' - | 'mckinsey' - | 'datawrapper' - | 'powerbi'; - -export interface ReferenceCase { - id: string; - house: ReferenceHouse; - houseLabel: string; - title: string; - /** Where the visual language comes from. */ - sourceNote: string; - /** Design parameters this reference is meant to demonstrate/transfer. */ - principles: string[]; - /** A coloured marker strip drawn above the chart (e.g. Economist red tab). */ - tab?: string; - /** Outer tile background, so a dark house reads correctly behind the chart. */ - tile?: string; - width: number; - height: number; - spec: any; -} - -export const REFERENCE_HOUSE_ORDER: ReferenceHouse[] = [ - 'economist', - 'nyt', - 'nature', - 'mckinsey', - 'datawrapper', - 'powerbi', -]; - -export const REFERENCE_HOUSE_LABEL: Record = { - economist: 'The Economist', - nyt: 'New York Times', - nature: 'Nature', - mckinsey: 'McKinsey', - datawrapper: 'Datawrapper', - powerbi: 'Power BI', -}; - -// --------------------------------------------------------------------------- -// Shared tiny datasets -// --------------------------------------------------------------------------- - -const YEARS = [2015, 2016, 2017, 2018, 2019, 2020, 2021, 2022, 2023]; - -function series(name: string, vals: number[]) { - return YEARS.map((year, i) => ({ year, series: name, value: vals[i] })); -} - -// --------------------------------------------------------------------------- -// The Economist — time-series line, pale blue panel, red top tab, right y-axis -// --------------------------------------------------------------------------- - -const economistLine: ReferenceCase = { - id: 'economist-line', - house: 'economist', - houseLabel: REFERENCE_HOUSE_LABEL.economist, - title: 'GDP per person', - sourceNote: - 'economist.com Graphic detail + ggthemes theme_economist: panel #d5e4eb, white horizontal gridlines, y-axis on the right with no domain line, one red series for emphasis.', - principles: [ - 'Wide-short canvas ~1.6:1 (small multiples run ~290×207px).', - 'Panel fill #d5e4eb; gridlines are WHITE and horizontal-only; no y domain/ticks.', - 'y-axis sits on the RIGHT; x-axis keeps a black baseline.', - 'Red (#e3120b) reserved for the one series that matters; everything else muted blue.', - 'Bold sans headline flush-left over a red tab; deck in grey.', - ], - tab: '#e3120b', - width: 380, - height: 236, - spec: { - config: { view: { stroke: null } }, - background: '#d5e4eb', - width: 320, - height: 176, - font: "'Helvetica Neue', Helvetica, Arial, sans-serif", - title: { - text: 'Pulling ahead', - subtitle: 'GDP per person, $’000 at PPP', - anchor: 'start', - fontSize: 15, - fontWeight: 700, - color: '#121317', - subtitleFontSize: 11, - subtitleColor: '#54585a', - offset: 12, - }, - data: { - values: [ - ...series('United States', [55, 57, 59, 62, 65, 63, 70, 76, 80]), - ...series('Euro area', [39, 40, 41, 43, 45, 43, 47, 50, 52]), - ], - }, - encoding: { - x: { - field: 'year', - type: 'ordinal', - axis: { - labelAngle: 0, - domainColor: '#121317', - domainWidth: 1, - tickColor: '#121317', - grid: false, - labelColor: '#121317', - labelFontSize: 11, - title: null, - values: [2015, 2017, 2019, 2021, 2023], - }, - }, - y: { - field: 'value', - type: 'quantitative', - scale: { domain: [30, 85] }, - axis: { - orient: 'right', - grid: true, - gridColor: '#ffffff', - gridWidth: 1.4, - domain: false, - ticks: false, - labelColor: '#121317', - labelFontSize: 11, - title: null, - tickCount: 5, - }, - }, - color: { - field: 'series', - type: 'nominal', - scale: { - domain: ['United States', 'Euro area'], - range: ['#e3120b', '#006ba2'], - }, - legend: null, - }, - }, - layer: [ - { mark: { type: 'line', strokeWidth: 3, interpolate: 'monotone' } }, - { - transform: [ - { filter: 'datum.year === 2023' }, - ], - mark: { type: 'text', align: 'right', dx: -4, dy: -8, fontSize: 11, fontWeight: 700 }, - encoding: { text: { field: 'series' } }, - }, - ], - }, -}; - -// --------------------------------------------------------------------------- -// New York Times — multi-line, direct end labels, muted ink, subtle grid -// --------------------------------------------------------------------------- - -const nytLine: ReferenceCase = { - id: 'nyt-line', - house: 'nyt', - houseLabel: REFERENCE_HOUSE_LABEL.nyt, - title: 'Direct-labelled trend lines', - sourceNote: - 'NYT graphics desk house style: near-square mobile-first canvas, no colour legend — series named at the line end, faint horizontal grid, serif deck over a sans body.', - principles: [ - 'Near-square canvas (~1.1:1) built mobile-first.', - 'No legend: label each line at its right end; reserve red for the lead series.', - 'Horizontal grid only, very light (#e4e4e4); drop the y domain + ticks.', - 'Extra right padding so the end labels have room to breathe.', - ], - width: 340, - height: 320, - spec: { - config: { view: { stroke: null } }, - background: '#ffffff', - width: 220, - height: 232, - padding: { right: 84, left: 4, top: 4, bottom: 4 }, - font: "'Helvetica Neue', Helvetica, Arial, sans-serif", - title: { - text: 'Streaming pulls even', - subtitle: 'Share of viewing hours, %', - anchor: 'start', - fontSize: 16, - fontWeight: 700, - color: '#121212', - font: 'Georgia, "Times New Roman", serif', - subtitleFont: "'Helvetica Neue', Helvetica, Arial, sans-serif", - subtitleFontSize: 12, - subtitleColor: '#6b6b6b', - offset: 12, - }, - data: { - values: [ - ...series('Streaming', [12, 16, 20, 25, 29, 34, 38, 40, 41]), - ...series('Cable', [42, 40, 38, 36, 34, 31, 28, 25, 23]), - ...series('Broadcast', [30, 28, 26, 24, 22, 21, 20, 19, 18]), - ], - }, - encoding: { - x: { - field: 'year', - type: 'quantitative', - scale: { domain: [2015, 2023], nice: false }, - axis: { - format: 'd', - grid: false, - domainColor: '#c9c9c9', - tickColor: '#c9c9c9', - labelColor: '#6b6b6b', - labelFontSize: 11, - title: null, - values: [2015, 2019, 2023], - }, - }, - y: { - field: 'value', - type: 'quantitative', - scale: { domain: [0, 45] }, - axis: { - grid: true, - gridColor: '#e4e4e4', - domain: false, - ticks: false, - labelColor: '#6b6b6b', - labelFontSize: 11, - title: null, - tickCount: 4, - }, - }, - color: { - field: 'series', - type: 'nominal', - scale: { - domain: ['Streaming', 'Cable', 'Broadcast'], - range: ['#c2352b', '#2f6b9a', '#8a8a8a'], - }, - legend: null, - }, - }, - layer: [ - { mark: { type: 'line', strokeWidth: 2.5 } }, - { - transform: [{ filter: 'datum.year === 2023' }], - mark: { type: 'text', align: 'left', dx: 6, fontSize: 11, fontWeight: 700 }, - encoding: { text: { field: 'series' } }, - }, - ], - }, -}; - -// --------------------------------------------------------------------------- -// Nature — small grouped bars with error bars, black frame, panel label -// --------------------------------------------------------------------------- - -const natureBars: ReferenceCase = { - id: 'nature-grouped', - house: 'nature', - houseLabel: REFERENCE_HOUSE_LABEL.nature, - title: 'Grouped bars with error bars', - sourceNote: - 'Nature figure conventions: single-column (~89 mm) compact panel, 6–7pt Arial, thin black L+B frame, no grid, Okabe–Ito colourblind-safe palette, bold panel letter, s.e.m. whiskers.', - principles: [ - 'Small, dense single-column panel — do NOT inflate the canvas.', - 'Tiny type (labels 7pt); thin black axis frame on left + bottom, no gridlines.', - 'Colourblind-safe categorical palette (Okabe–Ito).', - 'Bold lowercase panel letter top-left; error bars on every bar.', - ], - tab: undefined, - width: 260, - height: 220, - spec: { - config: { view: { stroke: null } }, - background: '#ffffff', - width: 190, - height: 150, - font: 'Arial, Helvetica, sans-serif', - title: { - text: 'a', - anchor: 'start', - fontSize: 15, - fontWeight: 700, - color: '#000000', - offset: 6, - }, - data: { - values: [ - { group: 'WT', cond: 'Control', value: 3.1, se: 0.3 }, - { group: 'WT', cond: 'Treated', value: 5.4, se: 0.4 }, - { group: 'KO', cond: 'Control', value: 2.7, se: 0.25 }, - { group: 'KO', cond: 'Treated', value: 3.2, se: 0.35 }, - ], - }, - encoding: { - x: { - field: 'group', - type: 'nominal', - axis: { - domainColor: '#000000', - domainWidth: 1, - tickColor: '#000000', - labelColor: '#000000', - labelFontSize: 8, - labelAngle: 0, - title: null, - grid: false, - }, - }, - y: { - field: 'value', - type: 'quantitative', - scale: { domain: [0, 6] }, - axis: { - domainColor: '#000000', - domainWidth: 1, - tickColor: '#000000', - grid: false, - labelColor: '#000000', - labelFontSize: 8, - title: 'mRNA (a.u.)', - titleColor: '#000000', - titleFontSize: 8, - titleFontWeight: 400, - tickCount: 4, - }, - }, - xOffset: { field: 'cond' }, - color: { - field: 'cond', - type: 'nominal', - scale: { domain: ['Control', 'Treated'], range: ['#0072b2', '#e69f00'] }, - legend: { - orient: 'top-right', - title: null, - labelFontSize: 8, - symbolSize: 40, - offset: 2, - }, - }, - }, - layer: [ - { mark: { type: 'bar' } }, - { - mark: { type: 'errorbar', ticks: true, color: '#000000' }, - encoding: { - y: { field: 'value', type: 'quantitative' }, - yError: { field: 'se' }, - }, - }, - ], - }, -}; - -// --------------------------------------------------------------------------- -// McKinsey — column chart, navy bars, direct value labels, no y-axis -// --------------------------------------------------------------------------- - -const mckinseyColumns: ReferenceCase = { - id: 'mckinsey-columns', - house: 'mckinsey', - houseLabel: REFERENCE_HOUSE_LABEL.mckinsey, - title: 'Value-labelled columns', - sourceNote: - 'McKinsey exhibit style: landscape slide proportions, thick navy (#051c2c) bars, value printed on top of each bar, no y-axis and no gridlines, one bar picked out in the blue accent, action-title headline.', - principles: [ - 'Wide slide canvas (~1.5:1); thick bars with generous whitespace.', - 'No y-axis, no gridlines — the number lives on top of the bar.', - 'Deep navy default; single accent (#2251ff) to highlight one column.', - 'Left-aligned action title, small grey source line.', - ], - width: 420, - height: 280, - spec: { - config: { view: { stroke: null } }, - background: '#ffffff', - width: 360, - height: 180, - font: "'Helvetica Neue', Helvetica, Arial, sans-serif", - title: { - text: 'Digital revenue nearly doubled in three years', - subtitle: 'Revenue, $bn', - anchor: 'start', - fontSize: 15, - fontWeight: 700, - color: '#051c2c', - subtitleFontSize: 11, - subtitleColor: '#5a6872', - offset: 14, - }, - data: { - values: [ - { q: '2020', value: 4.2 }, - { q: '2021', value: 5.1 }, - { q: '2022', value: 6.4 }, - { q: '2023', value: 8.0 }, - ], - }, - encoding: { - x: { - field: 'q', - type: 'nominal', - axis: { - domainColor: '#051c2c', - domainWidth: 1, - ticks: false, - labelColor: '#051c2c', - labelFontSize: 12, - labelPadding: 6, - labelAngle: 0, - title: null, - grid: false, - }, - }, - y: { - field: 'value', - type: 'quantitative', - axis: null, - }, - color: { - condition: { test: "datum.q === '2023'", value: '#2251ff' }, - value: '#051c2c', - }, - }, - layer: [ - { mark: { type: 'bar', size: 46 } }, - { - mark: { type: 'text', dy: -8, fontSize: 12, fontWeight: 700, color: '#051c2c' }, - encoding: { text: { field: 'value', format: '.1f' } }, - }, - ], - }, -}; - -// --------------------------------------------------------------------------- -// Datawrapper — horizontal bars, single blue, light grid, title/deck/source -// --------------------------------------------------------------------------- - -const datawrapperBars: ReferenceCase = { - id: 'datawrapper-bars', - house: 'datawrapper', - houseLabel: REFERENCE_HOUSE_LABEL.datawrapper, - title: 'Ranked horizontal bars', - sourceNote: - 'Datawrapper default theme: Roboto, single blue (#18a1cd), value axis with light #dcdcdc gridlines, bold #333 title over a grey deck, bars sorted, footer source line.', - principles: [ - 'Moderate landscape canvas (~1.25:1); bar height row-driven.', - 'Single brand blue #18a1cd; value gridlines light grey, category axis bare.', - 'Roboto/Arial; bold dark title, grey deck, small grey source footer.', - 'Bars sorted by value; direct value labels optional.', - ], - width: 380, - height: 300, - spec: { - config: { view: { stroke: null } }, - background: '#ffffff', - width: 210, - height: 190, - font: "Roboto, 'Helvetica Neue', Arial, sans-serif", - title: { - text: 'Where the visits came from', - subtitle: 'Share of sessions, %', - anchor: 'start', - fontSize: 15, - fontWeight: 700, - color: '#333333', - subtitleFontSize: 11, - subtitleColor: '#666666', - offset: 12, - }, - data: { - values: [ - { channel: 'Search', value: 41 }, - { channel: 'Direct', value: 27 }, - { channel: 'Social', value: 18 }, - { channel: 'Email', value: 9 }, - { channel: 'Referral', value: 5 }, - ], - }, - encoding: { - y: { - field: 'channel', - type: 'nominal', - sort: '-x', - axis: { - domain: false, - ticks: false, - labelColor: '#333333', - labelFontSize: 12, - title: null, - grid: false, - }, - }, - x: { - field: 'value', - type: 'quantitative', - axis: { - grid: true, - gridColor: '#dcdcdc', - domain: false, - ticks: false, - labelColor: '#666666', - labelFontSize: 10, - title: null, - tickCount: 4, - }, - }, - }, - mark: { type: 'bar', color: '#18a1cd', height: 20 }, - }, -}; - -// --------------------------------------------------------------------------- -// Power BI — dark dashboard tile, blue columns, light grid, Segoe UI -// --------------------------------------------------------------------------- - -const powerbiColumns: ReferenceCase = { - id: 'powerbi-columns', - house: 'powerbi', - houseLabel: REFERENCE_HOUSE_LABEL.powerbi, - title: 'Dashboard tile columns', - sourceNote: - 'Power BI default (dark) theme: 16:9 tile on a near-black canvas (#1b1a19), #118dff data colour, low-contrast #3b3a39 gridlines, Segoe UI, small tile title top-left.', - principles: [ - 'Wide 16:9 dashboard-tile canvas (~1.7:1).', - 'Dark panel #1b1a19; light text; single blue #118dff; faint grid #3b3a39.', - 'Segoe UI; compact tile title, no deck.', - ], - tile: '#1b1a19', - width: 440, - height: 260, - spec: { - config: { view: { stroke: null } }, - background: '#1b1a19', - width: 380, - height: 176, - font: "'Segoe UI', 'Helvetica Neue', Arial, sans-serif", - title: { - text: 'Sales by region', - anchor: 'start', - fontSize: 14, - fontWeight: 600, - color: '#f3f2f1', - offset: 10, - }, - data: { - values: [ - { region: 'North', value: 128 }, - { region: 'South', value: 94 }, - { region: 'East', value: 112 }, - { region: 'West', value: 76 }, - { region: 'Central', value: 88 }, - ], - }, - encoding: { - x: { - field: 'region', - type: 'nominal', - axis: { - domainColor: '#3b3a39', - ticks: false, - labelColor: '#c8c6c4', - labelFontSize: 11, - title: null, - grid: false, - labelAngle: 0, - }, - }, - y: { - field: 'value', - type: 'quantitative', - axis: { - grid: true, - gridColor: '#3b3a39', - domain: false, - ticks: false, - labelColor: '#a19f9d', - labelFontSize: 10, - title: null, - tickCount: 4, - }, - }, - }, - mark: { type: 'bar', color: '#118dff', size: 40 }, - }, -}; - -export const REFERENCE_CASES: ReferenceCase[] = [ - economistLine, - nytLine, - natureBars, - mckinseyColumns, - datawrapperBars, - powerbiColumns, -]; From 8060c3c79e17b7e43dd4415a0ed7b80ec71a3b66 Mon Sep 17 00:00:00 2001 From: Chenglong Wang Date: Fri, 31 Jul 2026 00:21:24 -0700 Subject: [PATCH 042/164] theme: add Power BI (light) house MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The polished default Power BI report look: a white tile, Segoe UI, the standard #118dff-led data palette, hairline light-grey gridlines (#ededed), dark text (#252423), legend to the right, latest point emphasised. It is the dark `powerbi` house flipped to a light surface — same furniture, same 16:9 tile proportions — so the same product is recognisable in either mode. Diverging neutral and status greens/reds are retuned for a light background. Registered as `powerbi-light`; the R2 grid picks it up as a new column automatically. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 33e4b998-4b58-4d7e-9cf8-c107b10e45c2 --- packages/flint-js/src/core/theme/presets.ts | 2 + .../src/core/theme/presets/powerbi-light.ts | 194 ++++++++++++++++++ 2 files changed, 196 insertions(+) create mode 100644 packages/flint-js/src/core/theme/presets/powerbi-light.ts diff --git a/packages/flint-js/src/core/theme/presets.ts b/packages/flint-js/src/core/theme/presets.ts index 63b8a528..e47ff428 100644 --- a/packages/flint-js/src/core/theme/presets.ts +++ b/packages/flint-js/src/core/theme/presets.ts @@ -16,6 +16,7 @@ import { nature } from './presets/nature'; import { mckinsey } from './presets/mckinsey'; import { datawrapper } from './presets/datawrapper'; import { powerbi } from './presets/powerbi'; +import { powerbiLight } from './presets/powerbi-light'; export const THEME_PRESETS: Record = { nyt, @@ -24,6 +25,7 @@ export const THEME_PRESETS: Record = { mckinsey, datawrapper, powerbi, + 'powerbi-light': powerbiLight, }; /** The catalogue, without the specs — enough to choose by. */ diff --git a/packages/flint-js/src/core/theme/presets/powerbi-light.ts b/packages/flint-js/src/core/theme/presets/powerbi-light.ts new file mode 100644 index 00000000..bb5c122a --- /dev/null +++ b/packages/flint-js/src/core/theme/presets/powerbi-light.ts @@ -0,0 +1,194 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import type { ThemePreset } from '../types'; + +/** + * Power BI — light. + * + * The default polished Power BI report look: a white tile, Segoe UI, the + * standard #118dff-led data palette, hairline light-grey gridlines, legend to + * the right, the latest point emphasised. The dark `powerbi` house flipped to + * a light surface — same furniture, same proportions, different ink — so a + * reader recognises the same product in either mode. + */ +export const powerbiLight: ThemePreset = { + id: 'powerbi-light', + label: "Power BI (light)", + description: "Light dashboard tile: white canvas, Segoe UI, hairline grid, legend to the right.", + guidance: [ + "- Leave `title` out where the tile sits under its own caption — the axis titles come back to name the measure.", + "- Colour can tell 6 categories apart.", + ].join('\n'), + spec: { + "id": "powerbi-light", + "label": "Power BI (light)", + "ink": { + "surface": { + "source": "house", + "canvas": "#ffffff", + "plot": "#ffffff", + "panel": "#faf9f8" + }, + "text": { + "primary": "#252423", + "secondary": "#605e5c", + "muted": "#a19f9d", + "inverse": "#ffffff" + }, + "structure": { + "grid": "#ededed", + "axis": "#d2d0ce", + "rule": "#d2d0ce" + }, + "series": { + "single": "#118dff", + "categorical": [ + "#118dff", + "#12239e", + "#e66c37", + "#6b007b", + "#e044a7", + "#744ec2" + ], + "diverging": { + "stops": [ + "#118dff", + "#7bb8f5", + "#e1dfdd", + "#e59866", + "#d64550" + ], + "neutral": "#e1dfdd", + "space": "lab", + "endpointsAgainstSurface": true, + "consumption": "interpolate" + }, + "status": { + "positive": "#107c10", + "negative": "#d13438", + "neutral": "#a19f9d" + }, + "selection": { + "signed": "diverging", + "statusUse": "thresholdOnly", + "redundantWithFacet": "single" + } + }, + "accent": "#118dff" + }, + "type": { + "minSize": 8, + "headline": { + "family": "'Segoe UI', system-ui, sans-serif", + "size": "text.200", + "weight": "semibold", + "color": "#252423" + }, + "display": { + "family": "'Segoe UI', system-ui, sans-serif", + "size": "text.hero900", + "weight": "semibold" + }, + "axisLabel": { + "family": "'Segoe UI', system-ui, sans-serif", + "size": "text.100", + "color": "#605e5c" + }, + "keyLabel": { + "size": "text.100", + "color": "#605e5c" + } + }, + "structure": { + "axis": { + "categorical": { + "line": "omit", + "ticks": "omit", + "tickLabels": "sparse" + }, + "measure": { + "line": "omit", + "ticks": "omit", + "tickDensity": "sparse" + } + }, + "grid": { + "measure": "quiet", + "category": "omit", + "style": "solid" + }, + "frame": "omit", + "baseline": "quiet" + }, + "marks": { + "strokeWeight": 2.2, + "strokeCap": "square", + "minSize": 1.5, + "separator": { + "presence": "hairline", + "source": "surface", + "width": 1 + }, + "slice": { + "gap": 1.5 + }, + "trailingFill": { + "presence": "quiet", + "opacity": 0.18 + }, + "reference": { + "presence": "full", + "style": "tick", + "label": true, + "weight": 2 + } + }, + "labels": { + "truncation": "never" + }, + "legend": { + "show": "always", + "placement": [ + "right", + "bottom" + ], + "title": "omit", + "gradientLength": 90, + "suppressWhenValuesPrinted": false + }, + "dataLabels": { + "show": "whenTheyFit", + "placement": "atMark", + "inkMode": "contrastWithMark" + }, + "annotation": { + "axisTitles": "omit", + "unit": "everyTick", + "pointEmphasis": "latest", + "numberFormat": { + "precision": "auto" + } + }, + "facets": { + "header": { + "presence": "full", + "style": "flushLabel", + "fieldTitle": "omit" + }, + "panelFrame": "omit", + "axisRepetition": "edgeOnly", + "preferredColumns": 4, + "sharedScale": "whenComparable" + }, + "layout": { + "density": "compact", + "titleBlock": { + "anchor": "start" + } + }, + "compileDefaults": { + "baseSize": { "width": 480, "height": 280 } + } + }, +}; From 8782e3c56765676106310240436706c416346663 Mon Sep 17 00:00:00 2001 From: Chenglong Wang Date: Fri, 31 Jul 2026 00:32:09 -0700 Subject: [PATCH 043/164] theme: make title-block vertical rhythm a per-house lever The title block's spacing was hardcoded in the realizer: title->chart offset = headline*0.9 and headline->deck padding = deck*0.6, identical for every house. A house's whitespace personality never reached the title zone, so a slide-style exhibit (McKinsey) packed its headline as tightly as a dense figure (Nature) or a dashboard tile (Power BI). Add ThemeLayout.titleBlock.gap and .deckGap ('tight'|'normal'|'loose') enums, grounded into DesignDecisions.title.offset / .deckPadding via TITLE_GAP / DECK_GAP multiplier maps in ground.ts, and consumed by the realizer. Defaults (normal/normal) reproduce the previous ratios, so unset houses are unchanged. Tune per house: mckinsey loose/loose (airy exhibit), economist tight gap, nature + powerbi + powerbi-light tight/tight (compact figures and tiles), nyt tight deckGap (deck hugs the headline), datawrapper default. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 33e4b998-4b58-4d7e-9cf8-c107b10e45c2 --- packages/flint-js/src/core/theme/ground.ts | 10 ++++++++++ .../src/core/theme/presets/economist.ts | 3 ++- .../flint-js/src/core/theme/presets/mckinsey.ts | 4 +++- .../flint-js/src/core/theme/presets/nature.ts | 4 +++- packages/flint-js/src/core/theme/presets/nyt.ts | 3 ++- .../src/core/theme/presets/powerbi-light.ts | 4 +++- .../flint-js/src/core/theme/presets/powerbi.ts | 4 +++- packages/flint-js/src/core/theme/types.ts | 17 ++++++++++++++++- packages/flint-js/src/vegalite/theme.ts | 4 ++-- 9 files changed, 44 insertions(+), 9 deletions(-) diff --git a/packages/flint-js/src/core/theme/ground.ts b/packages/flint-js/src/core/theme/ground.ts index 5e6d337e..62675ea5 100644 --- a/packages/flint-js/src/core/theme/ground.ts +++ b/packages/flint-js/src/core/theme/ground.ts @@ -310,6 +310,14 @@ const DISTRIBUTION_SHAPE_CHARTS = new Set(['Violin Plot', 'Density Plot']); // gutter, not a base the bars stand on. const TABLE_CHARTS = new Set(['Bar Table']); +// The title block's vertical rhythm, as a multiple of the headline / deck font +// size. A house's whitespace personality reaches the title here: `tight` packs +// the chart up under the headline (a dense figure, a dashboard tile); `loose` +// gives an action title room to breathe (a slide exhibit). `normal` preserves +// the ratios the realizer used before the block was expressible. +const TITLE_GAP: Record<'tight' | 'normal' | 'loose', number> = { tight: 0.45, normal: 0.9, loose: 1.7 }; +const DECK_GAP: Record<'tight' | 'normal' | 'loose', number> = { tight: 0.25, normal: 0.55, loose: 1.05 }; + function distinctCount(table: any[], field: string | undefined): number { if (!field) return 0; const seen = new Set(); @@ -1232,6 +1240,8 @@ export function groundTheme(themeIn: ThemeSpec, ctx: GroundingContext): DesignDe anchor: theme.layout?.titleBlock?.anchor ?? 'start', headline, deck, + offset: Math.round((headline.fontSize ?? 14) * TITLE_GAP[theme.layout?.titleBlock?.gap ?? 'normal']), + deckPadding: Math.round((deck.fontSize ?? 11) * DECK_GAP[theme.layout?.titleBlock?.deckGap ?? 'normal']), }, axes, frame, diff --git a/packages/flint-js/src/core/theme/presets/economist.ts b/packages/flint-js/src/core/theme/presets/economist.ts index 8e0f7bc0..c4df2e59 100644 --- a/packages/flint-js/src/core/theme/presets/economist.ts +++ b/packages/flint-js/src/core/theme/presets/economist.ts @@ -155,7 +155,8 @@ export const economist: ThemePreset = { "layout": { "density": "compact", "titleBlock": { - "anchor": "start" + "anchor": "start", + "gap": "tight" } }, "compileDefaults": { diff --git a/packages/flint-js/src/core/theme/presets/mckinsey.ts b/packages/flint-js/src/core/theme/presets/mckinsey.ts index 8c589b55..49c6c7d3 100644 --- a/packages/flint-js/src/core/theme/presets/mckinsey.ts +++ b/packages/flint-js/src/core/theme/presets/mckinsey.ts @@ -167,7 +167,9 @@ export const mckinsey: ThemePreset = { "layout": { "density": "airy", "titleBlock": { - "anchor": "start" + "anchor": "start", + "gap": "loose", + "deckGap": "loose" }, "bandStep": 80 }, diff --git a/packages/flint-js/src/core/theme/presets/nature.ts b/packages/flint-js/src/core/theme/presets/nature.ts index 5a439868..ca227d88 100644 --- a/packages/flint-js/src/core/theme/presets/nature.ts +++ b/packages/flint-js/src/core/theme/presets/nature.ts @@ -179,7 +179,9 @@ export const nature: ThemePreset = { "density": "compact", "targetWidth": 252, "titleBlock": { - "anchor": "start" + "anchor": "start", + "gap": "tight", + "deckGap": "tight" }, "bandStep": 46 }, diff --git a/packages/flint-js/src/core/theme/presets/nyt.ts b/packages/flint-js/src/core/theme/presets/nyt.ts index 12b7f1bb..66d1750b 100644 --- a/packages/flint-js/src/core/theme/presets/nyt.ts +++ b/packages/flint-js/src/core/theme/presets/nyt.ts @@ -163,7 +163,8 @@ export const nyt: ThemePreset = { "layout": { "density": "normal", "titleBlock": { - "anchor": "start" + "anchor": "start", + "deckGap": "tight" } }, "compileDefaults": { diff --git a/packages/flint-js/src/core/theme/presets/powerbi-light.ts b/packages/flint-js/src/core/theme/presets/powerbi-light.ts index bb5c122a..d0099821 100644 --- a/packages/flint-js/src/core/theme/presets/powerbi-light.ts +++ b/packages/flint-js/src/core/theme/presets/powerbi-light.ts @@ -184,7 +184,9 @@ export const powerbiLight: ThemePreset = { "layout": { "density": "compact", "titleBlock": { - "anchor": "start" + "anchor": "start", + "gap": "tight", + "deckGap": "tight" } }, "compileDefaults": { diff --git a/packages/flint-js/src/core/theme/presets/powerbi.ts b/packages/flint-js/src/core/theme/presets/powerbi.ts index f55c18d0..a5e90798 100644 --- a/packages/flint-js/src/core/theme/presets/powerbi.ts +++ b/packages/flint-js/src/core/theme/presets/powerbi.ts @@ -180,7 +180,9 @@ export const powerbi: ThemePreset = { "layout": { "density": "compact", "titleBlock": { - "anchor": "start" + "anchor": "start", + "gap": "tight", + "deckGap": "tight" } }, "compileDefaults": { diff --git a/packages/flint-js/src/core/theme/types.ts b/packages/flint-js/src/core/theme/types.ts index 24b79afb..b22250d6 100644 --- a/packages/flint-js/src/core/theme/types.ts +++ b/packages/flint-js/src/core/theme/types.ts @@ -342,7 +342,18 @@ export interface ThemeFacets { export interface ThemeLayout { density?: 'compact' | 'normal' | 'airy'; targetWidth?: number; - titleBlock?: { anchor?: 'start' | 'middle' | 'end' }; + titleBlock?: { + anchor?: 'start' | 'middle' | 'end'; + /** + * The vertical gap between the title block and the chart below it — a + * house's whitespace personality reaching the headline. `tight` packs + * the chart up under the title (a dense figure, a dashboard tile); + * `loose` gives an action title room to breathe (a slide exhibit). + */ + gap?: 'tight' | 'normal' | 'loose'; + /** The vertical gap between the headline and its deck (subtitle). */ + deckGap?: 'tight' | 'normal' | 'loose'; + }; bandStep?: number; } @@ -626,6 +637,10 @@ export interface DesignDecisions { anchor: 'start' | 'middle' | 'end'; headline: ResolvedText; deck: ResolvedText; + /** Gap from the title block to the chart, in px. */ + offset: number; + /** Gap between the headline and its deck, in px. */ + deckPadding: number; }; /** Bound axes, keyed by screen channel. */ axes: { x?: ResolvedAxis; y?: ResolvedAxis }; diff --git a/packages/flint-js/src/vegalite/theme.ts b/packages/flint-js/src/vegalite/theme.ts index 42bd3e54..0cd97152 100644 --- a/packages/flint-js/src/vegalite/theme.ts +++ b/packages/flint-js/src/vegalite/theme.ts @@ -215,12 +215,12 @@ function applyTypography(config: any, d: DesignDecisions): void { ...(h.fontStyle ? { fontStyle: h.fontStyle } : {}), color: h.color, anchor: d.title.anchor, - offset: Math.round((h.fontSize ?? 14) * 0.9), + offset: d.title.offset, subtitleFont: deck.font, subtitleFontSize: deck.fontSize, ...(deck.fontStyle ? { subtitleFontStyle: deck.fontStyle } : {}), subtitleColor: deck.color, - subtitlePadding: Math.round((deck.fontSize ?? 11) * 0.6), + subtitlePadding: d.title.deckPadding, }; // A start-anchored headline belongs at the edge of the *graphic*, not at // the left edge of the plotting rectangle — otherwise the width of the From 09ed692fca7ad6268f275b61e5a4fd5a8a3916fe Mon Sep 17 00:00:00 2001 From: jason-zl190 Date: Sat, 18 Jul 2026 18:25:34 +0800 Subject: [PATCH 044/164] feat(chartjs): add Lollipop Chart template Port the Lollipop template to the Chart.js backend, mirroring the ECharts implementation: a thin bar dataset as the stem (barThickness 1.5, black, same as the ECharts stem) plus a point-only line dataset as the dot, on the shared category scale. Supports horizontal transposition (indexAxis y), color grouping with one dot dataset per group, and the dotSize property on the same 6-16px visual scale as ECharts. The stem dataset is filtered out of legend and tooltip. - register in cjsTemplateDefs (Bar group, same position as ECharts) - tests: 8 cases (registry, stem/dot shape, zero baseline, transpose, color groups, legend/tooltip filters, dotSize mapping, 3-backend parity) - docs: regenerate reference-chartjs.md (npm run gen:reference); add Lollipop to the SKILL.md Chart.js coverage list (+ synced asset) Co-Authored-By: Claude Fable 5 --- agent-skills/flint-chart-author/SKILL.md | 4 +- docs/reference-chartjs.md | 10 +- .../flint-js/src/chartjs/templates/index.ts | 3 +- .../src/chartjs/templates/lollipop.ts | 141 ++++++++++++++++++ .../flint-js/tests/lollipop-chartjs.test.ts | 113 ++++++++++++++ .../assets/flint-chart-author.SKILL.md | 4 +- 6 files changed, 269 insertions(+), 6 deletions(-) create mode 100644 packages/flint-js/src/chartjs/templates/lollipop.ts create mode 100644 packages/flint-js/tests/lollipop-chartjs.test.ts diff --git a/agent-skills/flint-chart-author/SKILL.md b/agent-skills/flint-chart-author/SKILL.md index 52d1adc1..5a707345 100644 --- a/agent-skills/flint-chart-author/SKILL.md +++ b/agent-skills/flint-chart-author/SKILL.md @@ -251,8 +251,8 @@ support a subset (verify if targeting a non-VL backend): `"Funnel"`, `"Treemap"`, `"Sunburst"`, `"Sankey"`, `"Parallel Coordinates"`, `"Graph"`, `"Tree"`. - **Chart.js** supports: Scatter, Bubble, Bar, Grouped Bar, Stacked Bar, - Combo, Line, Bump, Area, Range Area, Pie, Doughnut, Histogram, Radar, Rose, - Slope, Connected Scatter. + Lollipop, Bump, Combo, Line, Area, Range Area, Pie, Doughnut, Histogram, + Radar, Rose, Slope, Connected Scatter. You do not need to call the library or inspect its source to author the input — pick from this table. diff --git a/docs/reference-chartjs.md b/docs/reference-chartjs.md index 4fece66b..20a01d99 100644 --- a/docs/reference-chartjs.md +++ b/docs/reference-chartjs.md @@ -6,7 +6,7 @@ The Chart.js backend is the lightweight embedding target for common chart famili ## What this page covers -This reference lists the 21 chart types currently supported by the Chart.js backend, grouped into 5 categories. Each chart entry shows: +This reference lists the 22 chart types currently supported by the Chart.js backend, grouped into 5 categories. Each chart entry shows: - **Encoding channels** — the visual roles accepted in `chart_spec.encodings`, such as `x`, `y`, `color`, `size`, `column`, or `row`. - **Options** — template-specific `chart_spec.chartProperties` keys, including control type, domain, default, availability, and description. @@ -85,6 +85,14 @@ _No template-specific parameters._ _No template-specific parameters._ +### ![](chart-icon-lollipop.svg) Lollipop Chart + +**Encoding channels:** `x`, `y`, `color`, `column`, `row` + +| Parameter | Control | Domain | Default | Availability | Description | +|---|---|---|---|---|---| +| `dotSize` | number | 20 – 300 (step 10) | `80` | always | Size of the dot mark. | + ### ![](chart-icon-combo.svg) Combo Chart **Encoding channels:** `x`, `y`, `column`, `row` diff --git a/packages/flint-js/src/chartjs/templates/index.ts b/packages/flint-js/src/chartjs/templates/index.ts index 0a54c051..d35302c0 100644 --- a/packages/flint-js/src/chartjs/templates/index.ts +++ b/packages/flint-js/src/chartjs/templates/index.ts @@ -14,6 +14,7 @@ import { cjsConnectedScatterDef } from './connected-scatter'; import { cjsBubbleChartDef } from './bubble'; import { cjsStripPlotDef } from './jitter'; import { cjsBarChartDef, cjsStackedBarChartDef, cjsGroupedBarChartDef } from './bar'; +import { cjsLollipopChartDef } from './lollipop'; import { cjsComboChartDef } from './combo'; import { cjsLineChartDef } from './line'; import { cjsBumpChartDef } from './bump'; @@ -34,7 +35,7 @@ import { cjsWaterfallChartDef } from './waterfall'; */ export const cjsTemplateDefs: { [key: string]: ChartTemplateDef[] } = { 'Scatter & Point': [cjsScatterPlotDef, cjsConnectedScatterDef, cjsBubbleChartDef, cjsStripPlotDef], - 'Bar': [cjsBarChartDef, cjsGroupedBarChartDef, cjsStackedBarChartDef, cjsComboChartDef, cjsHistogramDef, cjsWaterfallChartDef, cjsGanttChartDef], + 'Bar': [cjsBarChartDef, cjsGroupedBarChartDef, cjsStackedBarChartDef, cjsLollipopChartDef, cjsComboChartDef, cjsHistogramDef, cjsWaterfallChartDef, cjsGanttChartDef], 'Line & Area': [cjsLineChartDef, cjsBumpChartDef, cjsSlopeChartDef, cjsAreaChartDef, cjsRangeAreaChartDef, cjsEcdfPlotDef], 'Part-to-Whole': [cjsPieChartDef, cjsDoughnutChartDef], 'Polar': [cjsRadarChartDef, cjsRoseChartDef], diff --git a/packages/flint-js/src/chartjs/templates/lollipop.ts b/packages/flint-js/src/chartjs/templates/lollipop.ts new file mode 100644 index 00000000..e53dded3 --- /dev/null +++ b/packages/flint-js/src/chartjs/templates/lollipop.ts @@ -0,0 +1,141 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** + * Chart.js Lollipop Chart — thin bar stem from 0 to value + dot at the end + * (mirror of echarts/templates/lollipop.ts and vegalite/templates/lollipop.ts). + * + * Chart.js has no rule mark, so the stem is a `bar` dataset with a fixed + * `barThickness`, and the dot is a `line` dataset with `showLine: false` so it + * rides the shared category scale (a `scatter` dataset would require numeric + * `{x, y}` points instead of category labels). + */ + +import { ChartTemplateDef, ChartPropertyDef } from '../../core/types'; +import { + extractCategories, + groupBy, + buildCategoryAlignedData, + getChartJsPalette, + getSeriesBorderColor, + detectAxes, +} from './utils'; +import { detectBandedAxisFromSemantics } from '../../core/axis-detection'; + +/** Stem styling mirrors the ECharts template: black, ~1.5px (the Vega-Lite rule look). */ +const STEM_COLOR = '#000000'; +const STEM_WIDTH_PX = 1.5; +/** Internal dataset label for the stem; filtered out of legend and tooltip. */ +const STEM_LABEL = '__stem__'; + +/** Same visual scale as the ECharts template: 6–16px dot diameter. */ +function dotRadiusFromProperty(dotSize: number): number { + const diameterPx = Math.max(6, Math.min(10 + (dotSize - 80) / 40, 16)); + return diameterPx / 2; +} + +export const cjsLollipopChartDef: ChartTemplateDef = { + chart: 'Lollipop Chart', + template: { mark: 'bar', encoding: {} }, + channels: ['x', 'y', 'color', 'column', 'row'], + markCognitiveChannel: 'length', + declareLayoutMode: (cs, table) => { + const result = detectBandedAxisFromSemantics(cs, table, { preferAxis: 'x' }); + return { + axisFlags: result ? { [result.axis]: { banded: true } } : { x: { banded: true } }, + resolvedTypes: result?.resolvedTypes, + }; + }, + instantiate: (spec, ctx) => { + const { channelSemantics, table, chartProperties } = ctx; + const { categoryAxis, valueAxis } = detectAxes(channelSemantics); + + const catField = channelSemantics[categoryAxis]?.field; + const valField = channelSemantics[valueAxis]?.field; + if (!catField || !valField || table.length === 0) return; + + const colorField = channelSemantics.color?.field; + const categories = extractCategories( + table, catField, channelSemantics[categoryAxis]?.ordinalSortOrder, + ); + const stemData = buildCategoryAlignedData(table, catField, valField, categories); + + const isHorizontal = categoryAxis === 'y'; + const pointRadius = dotRadiusFromProperty(Number(chartProperties?.dotSize ?? 80)); + const palette = getChartJsPalette(ctx, 'color'); + + const datasets: any[] = [{ + type: 'bar' as const, + label: STEM_LABEL, + data: stemData, + barThickness: STEM_WIDTH_PX, + backgroundColor: STEM_COLOR, + borderWidth: 0, + order: 2, + }]; + + const dotDataset = (label: string, data: (number | null)[], colorIndex: number) => ({ + type: 'line' as const, + label, + data, + showLine: false, + pointRadius, + pointHoverRadius: pointRadius + 2, + borderColor: getSeriesBorderColor(palette, colorIndex), + backgroundColor: getSeriesBorderColor(palette, colorIndex), + pointBorderColor: '#fff', + pointBorderWidth: 1, + order: 1, + }); + + if (colorField) { + let i = 0; + for (const [name, rows] of groupBy(table, colorField)) { + datasets.push(dotDataset( + name, buildCategoryAlignedData(rows, catField, valField, categories), i, + )); + i++; + } + } else { + datasets.push(dotDataset(valField, stemData, 0)); + } + + const zeroDecision = channelSemantics[valueAxis]?.zero; + const config: any = { + type: 'bar', + data: { labels: categories, datasets }, + options: { + responsive: true, + maintainAspectRatio: false, + ...(isHorizontal ? { indexAxis: 'y' as const } : {}), + scales: { + [categoryAxis]: { + title: { display: true, text: catField }, + }, + [valueAxis]: { + type: 'linear' as const, + beginAtZero: zeroDecision ? zeroDecision.zero !== false : true, + title: { display: true, text: valField }, + }, + }, + plugins: { + legend: { + display: !!colorField, + labels: { filter: (item: any) => item.text !== STEM_LABEL }, + }, + tooltip: { + enabled: true, + filter: (item: any) => item.dataset?.label !== STEM_LABEL, + }, + }, + }, + }; + + Object.assign(spec, config); + delete spec.mark; + delete spec.encoding; + }, + properties: [ + { key: 'dotSize', label: 'Dot Size', type: 'continuous', min: 20, max: 300, step: 10, defaultValue: 80 }, + ] as ChartPropertyDef[], +}; diff --git a/packages/flint-js/tests/lollipop-chartjs.test.ts b/packages/flint-js/tests/lollipop-chartjs.test.ts new file mode 100644 index 00000000..b9d4ee4c --- /dev/null +++ b/packages/flint-js/tests/lollipop-chartjs.test.ts @@ -0,0 +1,113 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { describe, it, expect } from 'vitest'; +import { assembleChartjs, assembleECharts, assembleVegaLite, cjsGetTemplateDef } from '../src'; + +const DATA = [ + { region: 'East', revenue: 168 }, + { region: 'South', revenue: 167 }, + { region: 'North', revenue: 145 }, +]; + +const GROUPED_DATA = [ + { region: 'East', revenue: 168, year: '2024' }, + { region: 'South', revenue: 167, year: '2024' }, + { region: 'East', revenue: 120, year: '2025' }, + { region: 'South', revenue: 131, year: '2025' }, +]; + +function verticalInput(chartProperties?: Record) { + return { + data: { values: DATA }, + semantic_types: { region: 'Region', revenue: 'Amount' }, + chart_spec: { + chartType: 'Lollipop Chart', + encodings: { x: { field: 'region' }, y: { field: 'revenue' } }, + baseSize: { width: 400, height: 300 }, + ...(chartProperties ? { chartProperties } : {}), + }, + }; +} + +describe('Chart.js Lollipop Chart', () => { + it('is registered in the Chart.js template registry', () => { + expect(cjsGetTemplateDef('Lollipop Chart')).toBeDefined(); + }); + + it('builds a thin bar stem plus a point-only line dataset', () => { + const config = assembleChartjs(verticalInput()) as any; + expect(config.type).toBe('bar'); + expect(config.data.labels).toEqual(['East', 'South', 'North']); + + const [stem, dots] = config.data.datasets; + expect(stem.type).toBe('bar'); + expect(stem.barThickness).toBe(1.5); + expect(stem.data).toEqual([168, 167, 145]); + + expect(dots.type).toBe('line'); + expect(dots.showLine).toBe(false); + expect(dots.pointRadius).toBe(5); // dotSize default 80 → 10px diameter + expect(dots.data).toEqual([168, 167, 145]); + }); + + it('anchors the value axis at zero (stems grow from 0)', () => { + const config = assembleChartjs(verticalInput()) as any; + expect(config.options.scales.y.beginAtZero).toBe(true); + }); + + it('transposes to indexAxis "y" when the category is on y', () => { + const config = assembleChartjs({ + data: { values: DATA }, + semantic_types: { region: 'Region', revenue: 'Amount' }, + chart_spec: { + chartType: 'Lollipop Chart', + encodings: { x: { field: 'revenue' }, y: { field: 'region' } }, + baseSize: { width: 400, height: 300 }, + }, + }) as any; + expect(config.options.indexAxis).toBe('y'); + expect(config.options.scales.x.type).toBe('linear'); + expect(config.data.labels).toEqual(['East', 'South', 'North']); + }); + + it('adds one dot dataset per color group and shows the legend', () => { + const config = assembleChartjs({ + data: { values: GROUPED_DATA }, + semantic_types: { region: 'Region', revenue: 'Amount', year: 'Year' }, + chart_spec: { + chartType: 'Lollipop Chart', + encodings: { x: { field: 'region' }, y: { field: 'revenue' }, color: { field: 'year' } }, + baseSize: { width: 400, height: 300 }, + }, + }) as any; + + expect(config.data.datasets).toHaveLength(3); // stem + 2 groups + expect(config.data.datasets.slice(1).map((d: any) => d.label)).toEqual(['2024', '2025']); + expect(config.options.plugins.legend.display).toBe(true); + }); + + it('keeps the stem out of legend and tooltip', () => { + const config = assembleChartjs(verticalInput()) as any; + const stem = config.data.datasets[0]; + const { legend, tooltip } = config.options.plugins; + expect(legend.labels.filter({ text: stem.label })).toBe(false); + expect(tooltip.filter({ dataset: { label: stem.label } })).toBe(false); + expect(tooltip.filter({ dataset: { label: 'revenue' } })).toBe(true); + }); + + it('maps the dotSize property onto the point radius', () => { + const small = assembleChartjs(verticalInput({ dotSize: 20 })) as any; + const large = assembleChartjs(verticalInput({ dotSize: 300 })) as any; + expect(small.data.datasets[1].pointRadius).toBeLessThan(5); + expect(large.data.datasets[1].pointRadius).toBeGreaterThan(5); + expect(large.data.datasets[1].pointRadius).toBeLessThanOrEqual(8); // 16px diameter cap + }); + + it('compiles the same input on all three backends', () => { + const input = verticalInput(); + expect(() => assembleVegaLite(input)).not.toThrow(); + expect(() => assembleECharts(input)).not.toThrow(); + expect(() => assembleChartjs(input)).not.toThrow(); + }); +}); diff --git a/packages/flint-mcp/assets/flint-chart-author.SKILL.md b/packages/flint-mcp/assets/flint-chart-author.SKILL.md index 52d1adc1..5a707345 100644 --- a/packages/flint-mcp/assets/flint-chart-author.SKILL.md +++ b/packages/flint-mcp/assets/flint-chart-author.SKILL.md @@ -251,8 +251,8 @@ support a subset (verify if targeting a non-VL backend): `"Funnel"`, `"Treemap"`, `"Sunburst"`, `"Sankey"`, `"Parallel Coordinates"`, `"Graph"`, `"Tree"`. - **Chart.js** supports: Scatter, Bubble, Bar, Grouped Bar, Stacked Bar, - Combo, Line, Bump, Area, Range Area, Pie, Doughnut, Histogram, Radar, Rose, - Slope, Connected Scatter. + Lollipop, Bump, Combo, Line, Area, Range Area, Pie, Doughnut, Histogram, + Radar, Rose, Slope, Connected Scatter. You do not need to call the library or inspect its source to author the input — pick from this table. From a0ce619bc4fb675cd8f8d3cf1dd89384326a35a5 Mon Sep 17 00:00:00 2001 From: Chenglong Wang Date: Fri, 31 Jul 2026 09:05:27 -0700 Subject: [PATCH 045/164] playground(theme-lab): add Power BI (light) redesigns; drop redundant Flint column in grouped follow-ups MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Register the powerbi-light house in the theme-lab meta and add three hand-authored light-mode redesigns — browser-pie (pie), causes-death (horizontal bar) and ev-share (multi-line) — mirroring their dark powerbi counterparts with the ink flipped: white card, #252423 headline, #605e5c deck/labels, #ededed hairline grid, standard #118dff-led palette, right legend, white wedge separators. Since a case's Flint baseline is identical across every language it is themed in, the wall now draws it once — on the first tile of a group — and the follow-up tiles pair the hand-authored redesign against the compiled theme only (a hatched "same as above" placeholder keeps the redesign/compiled columns aligned down the group). The detail modal still shows the full baseline/redesign/compiled triptych. Verified: check-theme-lab renders all three cleanly, headline parity holds, site tsc clean. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 33e4b998-4b58-4d7e-9cf8-c107b10e45c2 --- site/src/playground/ThemeLab.tsx | 33 ++++- .../playground/theme-lab-assets/_themes.json | 18 ++- .../browser-pie.powerbi-light.json | 119 +++++++++++++++++ .../causes-death.powerbi-light.json | 106 +++++++++++++++ .../ev-share.powerbi-light.json | 121 ++++++++++++++++++ 5 files changed, 392 insertions(+), 5 deletions(-) create mode 100644 site/src/playground/theme-lab-assets/browser-pie.powerbi-light.json create mode 100644 site/src/playground/theme-lab-assets/causes-death.powerbi-light.json create mode 100644 site/src/playground/theme-lab-assets/ev-share.powerbi-light.json diff --git a/site/src/playground/ThemeLab.tsx b/site/src/playground/ThemeLab.tsx index 627c6ff6..c69adf71 100644 --- a/site/src/playground/ThemeLab.tsx +++ b/site/src/playground/ThemeLab.tsx @@ -294,7 +294,7 @@ function Thumb({ spec, bg, height }: { spec: any; bg: string; height: number }) * and the compiler's reading of the same ThemeSpec. Small enough to scan a whole * language in one screen; the argument lives in the popup, not here. */ -function WallTile({ row, onOpen }: { row: LabRow; onOpen: () => void }) { +function WallTile({ row, showFlint, onOpen }: { row: LabRow; showFlint: boolean; onOpen: () => void }) { const t = THEMES[row.theme]; const flint = useMemo(() => cleanSpec(row.flintSpec), [row.flintSpec]); const themed = useMemo(() => cleanSpec(row.themedSpec), [row.themedSpec]); @@ -322,7 +322,29 @@ function WallTile({ row, onOpen }: { row: LabRow; onOpen: () => void }) { }} >
- + {showFlint ? ( + + ) : ( +
+ Flint baseline is +
+ the same as above ↑ +
+ )} {compiled ? ( @@ -737,7 +759,9 @@ export function ThemeLab() { The wall is for scanning; click any tile for the full-size pair, the diff (the concrete list of things a design-theme layer would have to be able to express) and both raw specs. Tiles are grouped by chart, so a case themed in several languages - sits together. Specs live in site/src/playground/theme-lab-assets/, one + sits together. The Flint baseline is identical across a group, so it is drawn once, + on the first tile; the follow-ups pair the redesign against the compiled theme only. + Specs live in site/src/playground/theme-lab-assets/, one JSON per chart per theme, tagged with __theme__.

- {shown.map((row) => ( + {shown.map((row, i) => ( setOpenKey(`${row.id}-${row.theme}`)} /> ))} diff --git a/site/src/playground/theme-lab-assets/_themes.json b/site/src/playground/theme-lab-assets/_themes.json index 8f3451f9..c38b1387 100644 --- a/site/src/playground/theme-lab-assets/_themes.json +++ b/site/src/playground/theme-lab-assets/_themes.json @@ -1,5 +1,5 @@ { - "order": ["nyt", "economist", "nature", "mckinsey", "datawrapper", "powerbi"], + "order": ["nyt", "economist", "nature", "mckinsey", "datawrapper", "powerbi", "powerbi-light"], "themes": { "nyt": { "label": "NYT", @@ -96,6 +96,22 @@ "Legend in a fixed position as a reusable convention", "Status colours reserved for thresholds" ] + }, + "powerbi-light": { + "label": "Power BI (light)", + "alias": "product-dashboard-light", + "surface": "#ffffff", + "ink": "#252423", + "accent": "#118dff", + "swatches": ["#118dff", "#12239e", "#e66c37", "#6b007b", "#e044a7", "#744ec2"], + "intent": "The default polished light-mode report tile: a white card in a grid of cards, the same product as the dark theme with the ink flipped. Geometry stays stable as the data refreshes; legibility comes from a hairline grid and a fixed palette, not contrast against a dark ground.", + "signature": [ + "White card surface; #252423 ink, #ededed hairline grid", + "Compact 9.5pt Segoe UI — the tile is small on purpose", + "Solid low-contrast gridlines; marks stay dominant", + "Legend fixed on the right as a reusable convention", + "Status colours reserved for thresholds" + ] } } } diff --git a/site/src/playground/theme-lab-assets/browser-pie.powerbi-light.json b/site/src/playground/theme-lab-assets/browser-pie.powerbi-light.json new file mode 100644 index 00000000..353e7bd5 --- /dev/null +++ b/site/src/playground/theme-lab-assets/browser-pie.powerbi-light.json @@ -0,0 +1,119 @@ +{ + "$schema": "https://vega-lite.github.io/schema/vega-lite/v5.json", + "__theme__": "powerbi-light", + "__design__": [ + "The light counterpart of the dark tile — same furniture, ink flipped. The key is kept, on the right, because a tile is a query result: the category set changes when a slicer moves, and a legend redraws in place where labels pinned to particular wedges would have to be re-laid out.", + "Shares printed against the arcs anyway. Safari and Edge are both on 12 per cent and equal angles at different orientations cannot be compared — the one form where no language gets to trust the geometry, since there is no axis to fall back on.", + "Palette is the standard #118DFF-led data colours, unmodified — on a white card the mid-tones separate cleanly, so the dark theme's brightness lift is not needed.", + "Separators drawn in white — the card colour — so the gaps read as the surface showing through, not as a fifth ring of ink.", + "Labels at 9.5pt in #605C5C, the secondary ink: full-strength #252423 is reserved for the headline, and a step down keeps the wedge annotations subordinate to it.", + "Slice order left exactly as the baseline has it, clockwise from twelve o'clock in source order." + ], + "background": "#ffffff", + "padding": { "left": 10, "top": 8, "right": 12, "bottom": 8 }, + "title": { + "text": "Chrome holds two-thirds of the desktop market", + "subtitle": ["Desktop browser share, 2024, per cent"] + }, + "width": 215, + "height": 185, + "data": { + "values": [ + { "Browser": "Chrome", "Share": 65 }, + { "Browser": "Safari", "Share": 12 }, + { "Browser": "Edge", "Share": 12 }, + { "Browser": "Firefox", "Share": 6 }, + { "Browser": "Other", "Share": 5 } + ] + }, + "transform": [ + { "window": [{ "op": "row_number", "as": "__i" }], "frame": [null, null] }, + { + "window": [{ "op": "sum", "field": "Share", "as": "__cum" }], + "sort": [{ "field": "__i", "order": "ascending" }], + "frame": [null, 0] + }, + { "calculate": "(datum.__cum - datum.Share) / 100 * 2 * PI", "as": "__start" }, + { "calculate": "datum.__cum / 100 * 2 * PI", "as": "__end" }, + { "calculate": "(datum.__cum - datum.Share / 2) / 100 * 2 * PI", "as": "__mid" }, + { "calculate": "datum.Share + '%'", "as": "__pct" } + ], + "encoding": { + "color": { + "field": "Browser", + "type": "nominal", + "title": null, + "scale": { + "domain": ["Chrome", "Safari", "Edge", "Firefox", "Other"], + "range": ["#118dff", "#e66c37", "#12239e", "#6b007b", "#a19f9d"] + }, + "legend": { + "orient": "right", + "labelFontSize": 9, + "labelColor": "#605e5c", + "symbolType": "square", + "symbolSize": 70, + "symbolStrokeWidth": 0, + "offset": 6 + } + } + }, + "layer": [ + { + "mark": { "type": "arc", "outerRadius": 62, "stroke": "#ffffff", "strokeWidth": 1.5 }, + "encoding": { + "theta": { + "field": "__start", + "type": "quantitative", + "scale": { "domain": [0, 6.283185307179586], "range": [0, 6.283185307179586] } + }, + "theta2": { "field": "__end" } + } + }, + { + "transform": [{ "filter": "datum.__mid < PI" }], + "mark": { "type": "text", "radius": 70, "align": "left", "baseline": "middle", "fontSize": 9.5 }, + "encoding": { + "theta": { + "field": "__mid", + "type": "quantitative", + "scale": { "domain": [0, 6.283185307179586], "range": [0, 6.283185307179586] } + }, + "text": { "field": "__pct", "type": "nominal" }, + "color": { "value": "#605e5c" } + } + }, + { + "transform": [{ "filter": "datum.__mid >= PI" }], + "mark": { "type": "text", "radius": 70, "align": "right", "baseline": "middle", "fontSize": 9.5 }, + "encoding": { + "theta": { + "field": "__mid", + "type": "quantitative", + "scale": { "domain": [0, 6.283185307179586], "range": [0, 6.283185307179586] } + }, + "text": { "field": "__pct", "type": "nominal" }, + "color": { "value": "#605e5c" } + } + } + ], + "config": { + "background": "#ffffff", + "font": "'Segoe UI', 'Segoe UI Variable', system-ui, -apple-system, sans-serif", + "title": { + "font": "'Segoe UI', 'Segoe UI Variable', system-ui, sans-serif", + "fontSize": 12.5, + "fontWeight": 600, + "color": "#252423", + "anchor": "start", + "offset": 10, + "subtitleFont": "'Segoe UI', 'Segoe UI Variable', system-ui, sans-serif", + "subtitleFontSize": 10, + "subtitleColor": "#605e5c", + "subtitlePadding": 5 + }, + "view": { "stroke": null }, + "legend": { "labelFont": "'Segoe UI', 'Segoe UI Variable', system-ui, sans-serif" }, + "text": { "font": "'Segoe UI', 'Segoe UI Variable', system-ui, sans-serif" } + } +} diff --git a/site/src/playground/theme-lab-assets/causes-death.powerbi-light.json b/site/src/playground/theme-lab-assets/causes-death.powerbi-light.json new file mode 100644 index 00000000..5eedc591 --- /dev/null +++ b/site/src/playground/theme-lab-assets/causes-death.powerbi-light.json @@ -0,0 +1,106 @@ +{ + "$schema": "https://vega-lite.github.io/schema/vega-lite/v5.json", + "__theme__": "powerbi-light", + "__design__": [ + "The light counterpart of the dark tile. Axis and data labels are both kept — the tile is read twice, once across a room and once with a cursor on it, so it answers at both distances. The cost is admitted: this is the busiest of the light rows too.", + "White card, #118DFF fill. On a light page a saturated blue bar is the heaviest mark on the surface, so the ranking reads as printed ink — the opposite reading of the dark tile, where the same bar is the lightest thing and reads as emitted. The grid is a hairline #ededed, pale enough to sit behind the bars without competing.", + "The category gutter is uncapped. Flint clips five of the ten names to a fixed width; a tile that truncates is a tile that needs a tooltip to be usable, and a tooltip is not a chart.", + "Value labels at 9pt in #605E5C, the secondary ink — subordinate to the #252423 headline, legible against the bars.", + "Row order left exactly as the baseline has it — the source arrived ranked, so the bars descend without a theme touching the sort." + ], + "background": "#ffffff", + "padding": { "left": 10, "top": 8, "right": 16, "bottom": 8 }, + "title": { + "text": "What Americans die of", + "subtitle": ["Leading causes of death, United States, 2022, thousands of deaths"] + }, + "width": 250, + "height": 240, + "data": { + "values": [ + { "Cause": "Diseases of heart", "Deaths (thousands)": 703 }, + { "Cause": "Malignant neoplasms", "Deaths (thousands)": 608 }, + { "Cause": "Unintentional injuries", "Deaths (thousands)": 227 }, + { "Cause": "Cerebrovascular diseases", "Deaths (thousands)": 165 }, + { "Cause": "Chronic lower respiratory diseases", "Deaths (thousands)": 148 }, + { "Cause": "Alzheimer disease", "Deaths (thousands)": 120 }, + { "Cause": "Diabetes mellitus", "Deaths (thousands)": 102 }, + { "Cause": "Nephritis, nephrotic syndrome and nephrosis", "Deaths (thousands)": 58 }, + { "Cause": "Chronic liver disease and cirrhosis", "Deaths (thousands)": 55 }, + { "Cause": "Intentional self-harm (suicide)", "Deaths (thousands)": 49 } + ] + }, + "encoding": { + "y": { + "field": "Cause", + "type": "nominal", + "title": null, + "sort": null, + "axis": { + "labelLimit": 0, + "labelFontSize": 9.5, + "labelColor": "#252423", + "labelPadding": 6, + "domain": false, + "ticks": false, + "grid": false + } + }, + "x": { + "field": "Deaths (thousands)", + "type": "quantitative", + "title": null, + "scale": { "zero": true, "nice": false, "domain": [0, 810] }, + "axis": { + "values": [0, 200, 400, 600, 800], + "grid": true, + "gridColor": "#ededed", + "domain": false, + "ticks": false, + "labelFontSize": 9.5, + "labelColor": "#605e5c", + "labelPadding": 4 + } + } + }, + "layer": [ + { "mark": { "type": "bar", "color": "#118dff", "height": { "band": 0.6 } } }, + { + "mark": { + "type": "text", + "align": "left", + "baseline": "middle", + "dx": 5, + "fontSize": 9, + "color": "#605e5c" + }, + "encoding": { + "text": { "field": "Deaths (thousands)", "type": "quantitative", "format": "d" } + } + } + ], + "config": { + "background": "#ffffff", + "font": "'Segoe UI', 'Segoe UI Variable', system-ui, -apple-system, sans-serif", + "title": { + "font": "'Segoe UI', 'Segoe UI Variable', system-ui, sans-serif", + "fontSize": 12.5, + "fontWeight": 600, + "color": "#252423", + "anchor": "start", + "offset": 10, + "subtitleFont": "'Segoe UI', 'Segoe UI Variable', system-ui, sans-serif", + "subtitleFontSize": 10, + "subtitleColor": "#605e5c", + "subtitlePadding": 5 + }, + "view": { "stroke": null }, + "axis": { + "labelFont": "'Segoe UI', 'Segoe UI Variable', system-ui, sans-serif", + "titleFont": "'Segoe UI', 'Segoe UI Variable', system-ui, sans-serif", + "domain": false, + "ticks": false, + "grid": false + } + } +} diff --git a/site/src/playground/theme-lab-assets/ev-share.powerbi-light.json b/site/src/playground/theme-lab-assets/ev-share.powerbi-light.json new file mode 100644 index 00000000..4b08f754 --- /dev/null +++ b/site/src/playground/theme-lab-assets/ev-share.powerbi-light.json @@ -0,0 +1,121 @@ +{ + "$schema": "https://vega-lite.github.io/schema/vega-lite/v5.json", + "__theme__": "powerbi-light", + "__design__": [ + "The light counterpart of the dark tile. The key is kept on the right, where this language always puts it: the series set changes when a slicer moves, and a legend that redraws in place survives that, whereas labels pinned to the last point of each line rearrange themselves every time the data changes.", + "White card with a hairline #ededed grid — pale enough not to compete with the lines, the mirror of the dark tile where a mid-grey rule had to be dropped almost to the ground for the same reason.", + "Standard #118DFF-led data palette, unmodified and pinned to the baseline's series order — on white the mid-tones separate without the brightness lift the dark ground forced.", + "Points marked at each observation. The series is sampled unevenly, and on a tile that will be hovered for a tooltip the marker is also the target the cursor is looking for.", + "Labels at 9.5pt in #605E5C, the secondary ink, subordinate to the #252423 headline.", + "Series order left exactly as the baseline has it: Norway, China, Germany, United States." + ], + "background": "#ffffff", + "padding": { "left": 10, "top": 8, "right": 14, "bottom": 8 }, + "title": { + "text": "Electric cars as a share of new sales", + "subtitle": ["Norway, China, Germany and the United States, 2018–2023, per cent of new car sales"] + }, + "width": 265, + "height": 195, + "data": { + "values": [ + { "Year": "2018", "Country": "Norway", "EV share (%)": 49 }, + { "Year": "2020", "Country": "Norway", "EV share (%)": 75 }, + { "Year": "2022", "Country": "Norway", "EV share (%)": 88 }, + { "Year": "2023", "Country": "Norway", "EV share (%)": 93 }, + { "Year": "2018", "Country": "China", "EV share (%)": 4 }, + { "Year": "2020", "Country": "China", "EV share (%)": 6 }, + { "Year": "2022", "Country": "China", "EV share (%)": 29 }, + { "Year": "2023", "Country": "China", "EV share (%)": 38 }, + { "Year": "2018", "Country": "Germany", "EV share (%)": 2 }, + { "Year": "2020", "Country": "Germany", "EV share (%)": 13 }, + { "Year": "2022", "Country": "Germany", "EV share (%)": 31 }, + { "Year": "2023", "Country": "Germany", "EV share (%)": 25 }, + { "Year": "2018", "Country": "United States", "EV share (%)": 2 }, + { "Year": "2020", "Country": "United States", "EV share (%)": 2 }, + { "Year": "2022", "Country": "United States", "EV share (%)": 8 }, + { "Year": "2023", "Country": "United States", "EV share (%)": 10 } + ] + }, + "encoding": { + "x": { + "field": "Year", + "type": "temporal", + "title": null, + "scale": { "type": "utc" }, + "axis": { + "values": ["2018", "2020", "2022", "2023"], + "format": "%Y", + "grid": false, + "domain": false, + "ticks": false, + "labelFontSize": 9.5, + "labelColor": "#605e5c", + "labelPadding": 4 + } + }, + "y": { + "field": "EV share (%)", + "type": "quantitative", + "title": null, + "scale": { "zero": true, "nice": false, "domain": [0, 100] }, + "axis": { + "values": [0, 25, 50, 75, 100], + "format": "d", + "grid": true, + "gridColor": "#ededed", + "domain": false, + "ticks": false, + "labelFontSize": 9.5, + "labelColor": "#605e5c" + } + }, + "color": { + "field": "Country", + "type": "nominal", + "title": null, + "scale": { + "domain": ["Norway", "China", "Germany", "United States"], + "range": ["#118dff", "#e66c37", "#12239e", "#e044a7"] + }, + "legend": { + "orient": "right", + "labelFontSize": 9, + "labelColor": "#605e5c", + "symbolType": "stroke", + "symbolStrokeWidth": 3, + "symbolSize": 80, + "offset": 8 + } + } + }, + "layer": [ + { "mark": { "type": "line", "strokeWidth": 2 } }, + { "mark": { "type": "point", "filled": true, "size": 28 } } + ], + "config": { + "background": "#ffffff", + "font": "'Segoe UI', 'Segoe UI Variable', system-ui, -apple-system, sans-serif", + "title": { + "font": "'Segoe UI', 'Segoe UI Variable', system-ui, sans-serif", + "fontSize": 12.5, + "fontWeight": 600, + "color": "#252423", + "anchor": "start", + "offset": 10, + "subtitleFont": "'Segoe UI', 'Segoe UI Variable', system-ui, sans-serif", + "subtitleFontSize": 10, + "subtitleColor": "#605e5c", + "subtitlePadding": 5 + }, + "view": { "stroke": null }, + "axis": { + "labelFont": "'Segoe UI', 'Segoe UI Variable', system-ui, sans-serif", + "titleFont": "'Segoe UI', 'Segoe UI Variable', system-ui, sans-serif", + "domain": false, + "ticks": false, + "grid": false + }, + "legend": { "labelFont": "'Segoe UI', 'Segoe UI Variable', system-ui, sans-serif" } + } +} From 901b7c492504a3a51b93d0f3154432e2eeb42b78 Mon Sep 17 00:00:00 2001 From: Chenglong Wang Date: Fri, 31 Jul 2026 09:15:41 -0700 Subject: [PATCH 046/164] playground(theme-lab): fit modal charts to their pane, wrap instead of scroll MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The detail popup laid its three charts in a fixed 3-column grid whose cells could shrink below a chart's natural width, so each pane grew its own horizontal scrollbar and the charts read cramped. Switch the row to flex-wrap (side-by-side when the modal is wide, stacking to two-up or a single column when it is not) and render each chart through ScaleToFit with adaptiveHeight, which fits the chart to the pane width — scaling an oversized chart down rather than clipping or scrolling it, and never scaling a small one up past its designed size. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 33e4b998-4b58-4d7e-9cf8-c107b10e45c2 --- site/src/playground/ThemeLab.tsx | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/site/src/playground/ThemeLab.tsx b/site/src/playground/ThemeLab.tsx index c69adf71..96ab0902 100644 --- a/site/src/playground/ThemeLab.tsx +++ b/site/src/playground/ThemeLab.tsx @@ -242,7 +242,7 @@ function ThemeChip({ theme }: { theme: ThemeId }) { function SpecCell({ spec, dark, label }: { spec: any; dark: boolean; label: string }) { const cleaned = useMemo(() => cleanSpec(spec), [spec]); return ( -

+
- + + +
); @@ -536,10 +537,10 @@ function DetailModal({ row, onClose }: { row: LabRow; onClose: () => void }) {
From 838f7bd7982d821039537c0a3f6404ae7ecb7a8f Mon Sep 17 00:00:00 2001 From: Chenglong Wang Date: Fri, 31 Jul 2026 09:22:51 -0700 Subject: [PATCH 047/164] playground(theme-lab): simplify coverage section to plain bullets, refresh for 7th house Rewrite "Coverage, and what is still missing" as plain headings + bullet lists, dropping the three bordered GapCard boxes and the accent-ruled prose block. Also brings it up to date now that Power BI (light) is the seventh language and the pie/bar/line sets are each fully replicated: - "splits the six" / "all six print" -> houses described without a stale count; Power BI folded in as "light and dark", keeping both on the bar. - legend-vs-direct grouping now lists Power BI (light and dark) on the key side. - "a seventh language on a seventh chart" -> "six houses plus a light mode of one", framing the four proposed languages as new decision axes rather than more of the same. Remove the now-unused GapCard component. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 33e4b998-4b58-4d7e-9cf8-c107b10e45c2 --- site/src/playground/ThemeLab.tsx | 203 ++++++++++++++++--------------- 1 file changed, 103 insertions(+), 100 deletions(-) diff --git a/site/src/playground/ThemeLab.tsx b/site/src/playground/ThemeLab.tsx index 96ab0902..71311816 100644 --- a/site/src/playground/ThemeLab.tsx +++ b/site/src/playground/ThemeLab.tsx @@ -934,96 +934,122 @@ function CoverageNotes({ rows }: { rows: LabRow[] }) { const full = [...themesPerCase.entries()].filter(([, n]) => n >= THEME_ORDER.length); const once = [...themesPerCase.values()].filter((n) => n === 1).length; + const bulletList: React.CSSProperties = { + margin: '6px 0 18px', + paddingLeft: 18, + maxWidth: 860, + fontSize: 13, + lineHeight: 1.6, + color: siteTheme.textMuted, + }; + const heading: React.CSSProperties = { + fontSize: 13.5, + fontWeight: 600, + color: siteTheme.text, + margin: '18px 0 0', + }; + return (

Coverage, and what is still missing

-

+

{rows.length} redesigns across {coveredTypes.size} chart types and {THEME_ORDER.length}{' '} design languages, drawn from {index.length} Flint baselines. A case earns its place by forcing a decision no theme here has had to make yet — more charts is not the same as more evidence.

-

- The largest gap is not a missing chart.{' '} - {full.length === 0 - ? `No case carries all ${THEME_ORDER.length} languages, and ${once} of ${themesPerCase.size} are themed exactly once.` - : `${full.length} of ${themesPerCase.size} cases carry all ${THEME_ORDER.length} languages (${full - .map(([id]) => id) - .join(', ')}); ${once} are themed exactly once.`}{' '} - Themed once, a row cannot separate what the language decided from what the chart - demanded. -
-
- Three replicated sets sort the decisions into two kinds.{' '} - Print the values or keep the axis splits the six on the bar — NYT, McKinsey, - Datawrapper and Power BI print; the Economist and Nature trust the axis — and then stops - meaning anything: on the pie there is no axis to trust, and all six print.{' '} - Legend or direct labels holds: NYT, the Economist and McKinsey label directly on - both the line and the pie; Nature, Datawrapper and Power BI keep a key on both. The first - is the chart talking, the second is the house — and a single-case table would have called - both style. -
-
- So what a theme layer has to encode is an attitude to scaffolding, not a rule about - marks. Nature keeps the most, and is the only language that answers a problem by adding - an encoding rather than removing one. McKinsey keeps the least. -

+

The biggest gap is not a missing chart

+
    +
  • + {full.length === 0 + ? `No case carries all ${THEME_ORDER.length} languages, and ${once} of ${themesPerCase.size} are themed exactly once.` + : `${full.length} of ${themesPerCase.size} cases carry all ${THEME_ORDER.length} languages (${full + .map(([id]) => id) + .join(', ')}); ${once} are themed exactly once.`}{' '} + Themed once, a row cannot separate what the language decided from what the chart + demanded. +
  • +
  • + Print the values or keep the axis is the chart talking, not the house: on the + bar most houses print the number on the mark and the Economist and Nature trust the + axis instead (Power BI, in either mode, keeps both); on the pie there is no axis to + trust, so everyone prints. +
  • +
  • + Legend or direct labels is the house talking, and it holds across charts: NYT, + the Economist and McKinsey label directly on both the line and the pie; Nature, + Datawrapper and Power BI (light and dark) keep a key on both. +
  • +
  • + So what a theme layer has to encode is an attitude to scaffolding, not a rule about + marks. Nature keeps the most, and is the only language that answers a problem by adding + an encoding rather than removing one; McKinsey keeps the least. +
  • +
-
- - e.chartType).join(', ')}). The other ${untouched.length - untouchedNew.length} are repeats of covered types and buy nothing — a second scatter tests the same decisions as the first.`, - 'Calendar heatmaps and cycle plots — seasonality is a layout question no case here asks.', - 'Point-symbol maps. The choropleth settles class breaks and hue ramps; a size legend floating over a basemap is untested.', - 'Radial forms — donut, rose, radar sit unpaired. Angle and area are the hardest channels for a language to legislate, and the pie is the only one of the four any of these six has had to argue.', - 'Funnel and gauge — Plotly-only in Flint, so the baseline column cannot be produced at all.', - ]} - /> - -
+

Missing: structural situations

+
    +
  • + Dual-axis / combo. Two units in one frame is the hardest thing to theme — which axis + keeps the grid, which series takes the accent. Unreachable: Flint has no Vega-Lite + combo template, so there is no baseline to argue against. +
  • +
  • + Missing data. No dataset here has a hole in it. Break the line, interpolate, or shade + the gap — a newspaper and a journal answer differently. +
  • +
  • + High cardinality in colour. The fifty-state row tests fifty axis labels, which is + typography. It never tests what happens when hue itself runs out. +
  • +
+ +

Missing: chart types

+
    +
  • + Untouched baselines: {untouched.length}, but only {untouchedNew.length} add a chart + type never themed here ({untouchedNew.map((e) => e.chartType).join(', ')}). The other{' '} + {untouched.length - untouchedNew.length} are repeats of covered types and buy nothing — + a second scatter tests the same decisions as the first. +
  • +
  • Calendar heatmaps and cycle plots — seasonality is a layout question no case here asks.
  • +
  • + Point-symbol maps. The choropleth settles class breaks and hue ramps; a size legend + floating over a basemap is untested. +
  • +
  • + Radial forms — donut, rose, radar sit unpaired. Angle and area are the hardest channels + for a language to legislate, and the pie is the only one of the four any house here has + had to argue. +
  • +
  • Funnel and gauge — Plotly-only in Flint, so the baseline column cannot be produced at all.
  • +
+ +

Missing: theme languages

+
    +
  • + The {THEME_ORDER.length} here are six houses plus a light mode of one — the languages + below would each force a decision no current house makes, not just widen the table. +
  • +
  • + Print-mono — one ink, texture and weight only. Every rule that currently leans on hue + would have to be restated. +
  • +
  • High-density terminal — tiny type, dark ground, no whitespace, information over legibility.
  • +
  • + Accessibility-first — an explicit contrast floor and pattern fills, which would collide + productively with the Datawrapper rows. +
  • +
  • + Raw exploratory — deliberately unstyled and disposable. The null hypothesis: the point + below which theming is not worth doing. +
  • +
-
+
Baselines with no bespoke counterpart yet ({untouched.length}) @@ -1049,29 +1075,6 @@ function CoverageNotes({ rows }: { rows: LabRow[] }) { ); } -function GapCard({ title, tone, items }: { title: string; tone: 'done' | 'gap'; items: string[] }) { - return ( -
- {title} -
    - {items.map((s, i) => ( -
  • - {s} -
  • - ))} -
-
- ); -} - function filterBtn(active: boolean): React.CSSProperties { return { fontSize: 12, From 6d2a3cada6466858af3217c193eece5366323e65 Mon Sep 17 00:00:00 2001 From: Chenglong Wang Date: Fri, 31 Jul 2026 09:44:37 -0700 Subject: [PATCH 048/164] Grouped boxplot bands stretch to fill their lanes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A grouped discrete axis sizes its category band toward a per-item step target, so a band holding N dodged lanes was sized as if it held one. For thin grouped bars that is right, but a box-and-whisker glyph needs real room per lane — box-6x4 (6 countries x 4 quarters) compressed each box to ~11px on an 800px-capable canvas. Add a boxplot-scoped `groupBandFillsLanes` option: when set, the grouped band's elastic stretch target is `itemsPerGroup x step`, so the band stretches toward the space its lanes actually need (still bounded by `maxBandSize x itemsPerGroup` and the canvas budget). box-6x4 band step 66 -> 99px; ungrouped box-12 and grouped bars are unchanged. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 33e4b998-4b58-4d7e-9cf8-c107b10e45c2 --- packages/flint-js/src/core/compute-layout.ts | 10 ++++++++-- packages/flint-js/src/core/types.ts | 16 ++++++++++++++++ .../flint-js/src/vegalite/templates/scatter.ts | 2 +- 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/packages/flint-js/src/core/compute-layout.ts b/packages/flint-js/src/core/compute-layout.ts index 7b2da77a..30ef36fc 100644 --- a/packages/flint-js/src/core/compute-layout.ts +++ b/packages/flint-js/src/core/compute-layout.ts @@ -832,7 +832,10 @@ export function computeLayout( const itemsPerGroup = nominalCount.group; const defaultGroupStep = itemsPerGroup * maxStepSize; const minGroupStep = Math.max(Math.ceil(MIN_GROUP_GAP_PX / stepPaddingVal), 2 * itemsPerGroup); - const groupAxis = computeAxisStep(nominalCount.x, 0, subplotWidth, elasticParamsX); + const groupElasticX = options.groupBandFillsLanes + ? { ...elasticParamsX, defaultStepSize: elasticParamsX.defaultStepSize * itemsPerGroup } + : elasticParamsX; + const groupAxis = computeAxisStep(nominalCount.x, 0, subplotWidth, groupElasticX); const groupStep = Math.max(minGroupStep, Math.min(defaultGroupStep, groupAxis.step)); xStepSize = groupStep; xStepUnit = 'group'; @@ -848,7 +851,10 @@ export function computeLayout( const itemsPerGroup = nominalCount.group; const defaultGroupStep = itemsPerGroup * maxStepSize; const minGroupStep = Math.max(Math.ceil(MIN_GROUP_GAP_PX / stepPaddingVal), 2 * itemsPerGroup); - const groupAxis = computeAxisStep(nominalCount.y, 0, subplotHeight, elasticParamsY); + const groupElasticY = options.groupBandFillsLanes + ? { ...elasticParamsY, defaultStepSize: elasticParamsY.defaultStepSize * itemsPerGroup } + : elasticParamsY; + const groupAxis = computeAxisStep(nominalCount.y, 0, subplotHeight, groupElasticY); const groupStep = Math.max(minGroupStep, Math.min(defaultGroupStep, groupAxis.step)); yStepSize = groupStep; yStepUnit = 'group'; diff --git a/packages/flint-js/src/core/types.ts b/packages/flint-js/src/core/types.ts index 6775b623..ba031c03 100644 --- a/packages/flint-js/src/core/types.ts +++ b/packages/flint-js/src/core/types.ts @@ -1268,6 +1268,22 @@ export interface AssembleOptions { * Defaults to {@link defaultBandSize} (no expansion beyond the base band). */ maxBandSize?: number; + /** + * When a discrete axis is **grouped** (dodged lanes within each category + * band), by default the band's elastic stretch target is the *per-item* + * step — so a category holding N lanes is only sized as if it held one. + * For thin marks (grouped bars) that's fine: 4 lanes share a ~66px band + * comfortably. But wide marks — a box-and-whisker glyph — need real room + * per lane, and the per-item target leaves grouped boxplots compressed on + * an otherwise roomy canvas. + * + * When set, the grouped band instead targets `itemsPerGroup × step`, so + * the category band stretches to give each lane its full width (still + * bounded by `maxBandSize × itemsPerGroup` and the canvas budget). Scoped + * to templates whose grouped glyph is wide (boxplot); left off for bars so + * their tuned grouped spacing does not change. + */ + groupBandFillsLanes?: boolean; /** * Backend-native base font size (px) for axis **tick labels**, at a 300px * reference canvas. The core scales it subtly with canvas size and uses it diff --git a/packages/flint-js/src/vegalite/templates/scatter.ts b/packages/flint-js/src/vegalite/templates/scatter.ts index d5dafeec..29a21f43 100644 --- a/packages/flint-js/src/vegalite/templates/scatter.ts +++ b/packages/flint-js/src/vegalite/templates/scatter.ts @@ -216,7 +216,7 @@ export const boxplotDef: ChartTemplateDef = { return { axisFlags: { [result.axis]: { banded: true } }, resolvedTypes: result.resolvedTypes, - paramOverrides: { defaultBandSize: 28 }, // box+whisker needs wider bands + paramOverrides: { defaultBandSize: 28, groupBandFillsLanes: true }, // box+whisker needs wider bands; grouped lanes each get full width colorActsAsGroup, // dodge-by-color → budget band per category, shrink lanes ...(groupLaneCount ? { groupLaneCount } : {}), }; From dfdb6c6fd7efff49f125697bc99bd4c5a9d17dff Mon Sep 17 00:00:00 2001 From: Chenglong Wang Date: Fri, 31 Jul 2026 10:15:59 -0700 Subject: [PATCH 049/164] Tiered palettes + share-ordered "other" overflow for high cardinality MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit NYT (5 inks) and Nature (6) declared an overflow ink, so a 10-slice pie dropped half its wedges into one indistinguishable grey mass — and the grey fell on arbitrary categories, not the smallest. Houses without an overflow ink silently leaked to Vega-Lite's default tableau scheme. Three coordinated changes: 1. Theming — enrich NYT and Nature with an extended palette tier. `ink.series.categoricalExtended` carries a larger indexed set (12 inks) the house reaches for at higher cardinality, in the spirit of Tableau 10→20. NYT gets an editorial 12-colour set; Nature extends its Okabe-Ito core (the Nature Methods colourblind-safe standard) with well-separated Paul Tol hues. 2. Auto-upsize — grounding ranks the house's palette tiers by capacity and picks the smallest one that still gives every series its own ink, so a 10-series chart uses the 12-set rather than overflowing. 3. Principled overflow — past even the extended set, the largest `categorical.length` series by share keep a colour and the rest fold into the single overflow ink. Realization orders the colour domain by each category's share of the measure, so it is the *smallest* series that go grey (read as one contiguous tail), and the legend names only the coloured ones instead of listing 20 identical grey rows. pie-10 now shows 10 distinct house inks (was a grey blob) for both houses; pie-25 degrades to ~12 coloured + a grey tail; slope-crossings at n=8 gains distinct Nature colours (resolves the parked palette-repeat gap). Houses without categoricalExtended/overflow are unchanged. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 33e4b998-4b58-4d7e-9cf8-c107b10e45c2 --- packages/flint-js/src/core/theme/ground.ts | 31 +++++++-- .../flint-js/src/core/theme/presets/nature.ts | 14 ++++ .../flint-js/src/core/theme/presets/nyt.ts | 14 ++++ packages/flint-js/src/core/theme/types.ts | 24 +++++++ packages/flint-js/src/vegalite/theme.ts | 64 +++++++++++++++++++ 5 files changed, 143 insertions(+), 4 deletions(-) diff --git a/packages/flint-js/src/core/theme/ground.ts b/packages/flint-js/src/core/theme/ground.ts index 62675ea5..2e9a8cb1 100644 --- a/packages/flint-js/src/core/theme/ground.ts +++ b/packages/flint-js/src/core/theme/ground.ts @@ -1369,9 +1369,18 @@ function groundSeriesInk( const s = theme.ink.series ?? {}; const selection = s.selection ?? {}; const categorical = s.categorical ?? []; + const extended = s.categoricalExtended ?? []; const single = s.single ?? categorical[0] ?? theme.ink.accent ?? '#4c78a8'; const surfaceColour = theme.ink.surface?.plot ?? theme.ink.surface?.canvas ?? '#ffffff'; + // The house may name a larger indexed set for higher cardinality (Tableau + // 10→20). Rank the tiers by capacity so we can reach for the smallest one + // that still gives every series its own ink. + const tiers = [categorical, extended] + .filter((t) => t.length > 0) + .sort((a, b) => a.length - b.length); + const largestTier = tiers.length ? tiers[tiers.length - 1] : categorical; + const seriesChannel = bindings.seriesChannel; const seriesField = channelFact(ctx, seriesChannel)?.field; const seriesType = channelFact(ctx, seriesChannel)?.type; @@ -1477,9 +1486,23 @@ function groundSeriesInk( } if (signals.seriesCountKnown && count > categorical.length && categorical.length > 0) { + // Auto-upsize: an extended tier that still names every series is the + // right answer — reach for the smallest one that covers the count. + const fittingTier = tiers.find((t) => count <= t.length); + if (fittingTier && fittingTier.length > categorical.length) { + say('ink.series.categorical', + `${count} series past the core ${categorical.length} inks — the house's extended ${fittingTier.length}-colour set is used so each stays distinct`); + return { ...base, categorical: fittingTier, mode: 'categorical' }; + } + if (s.overflow) { + // Past even the extended set. The top inks by prominence name the + // largest series; every remaining ("other") series folds into the + // one overflow ink. Realization orders the domain by share so it is + // the smallest series that go grey, read as a single tail. say('ink.series.categorical', - `${count} series but the house declares ${categorical.length} — the rest take the overflow ink`); + `${count} series past the house's ${largestTier.length} inks — the largest ${largestTier.length} keep a colour, the rest fold into one "other" ink`); + return { ...base, categorical: largestTier, mode: 'categorical', overflowTail: true }; } else if (ctx.namesOnMarks === true) { // The chart prints the series name on the mark (a slopegraph's end // labels, a house that asked for it), so the names are already on @@ -1488,7 +1511,7 @@ function groundSeriesInk( // seven labels that say the same thing the words do. One ink, and // the reader reads the names. say('ink.series.categorical', - `${count} series against ${categorical.length} house inks, but the house names them on the mark — colour stops naming and takes the single ink`); + `${count} series against ${largestTier.length} house inks, but the house names them on the mark — colour stops naming and takes the single ink`); return { ...base, mode: 'single' }; } else { // An indexed set has a capacity, and past it the colours stop @@ -1497,8 +1520,8 @@ function groundSeriesInk( // said what the twenty-fifth thing looks like, and cycling is not // an answer — it is the same answer twice. say('ink.series.categorical', - `${count} series and the house declares ${categorical.length} with no overflow ink — colour cannot name them all, so the scale already on the chart stands`); - return { ...base, mode: 'categorical', exhausted: true }; + `${count} series and the house declares ${largestTier.length} with no overflow ink — colour cannot name them all, so the scale already on the chart stands`); + return { ...base, categorical: largestTier, mode: 'categorical', exhausted: true }; } } return { ...base, mode: 'categorical' }; diff --git a/packages/flint-js/src/core/theme/presets/nature.ts b/packages/flint-js/src/core/theme/presets/nature.ts index ca227d88..07a71b94 100644 --- a/packages/flint-js/src/core/theme/presets/nature.ts +++ b/packages/flint-js/src/core/theme/presets/nature.ts @@ -42,6 +42,20 @@ export const nature: ThemePreset = { "#56b4e9", "#d55e00" ], + "categoricalExtended": [ + "#0072b2", + "#e69f00", + "#009e73", + "#cc79a7", + "#56b4e9", + "#d55e00", + "#f0e442", + "#332288", + "#117733", + "#882255", + "#88ccee", + "#999933" + ], "diverging": { "stops": [ "#0072b2", diff --git a/packages/flint-js/src/core/theme/presets/nyt.ts b/packages/flint-js/src/core/theme/presets/nyt.ts index 66d1750b..ec50ccb4 100644 --- a/packages/flint-js/src/core/theme/presets/nyt.ts +++ b/packages/flint-js/src/core/theme/presets/nyt.ts @@ -43,6 +43,20 @@ export const nyt: ThemePreset = { "#7f6a9e", "#d9a441" ], + "categoricalExtended": [ + "#2f6b9a", + "#c2352b", + "#4a8b6f", + "#7f6a9e", + "#d9a441", + "#e27ea6", + "#3fae9e", + "#9ca13a", + "#8c6d31", + "#6b8fb3", + "#d07b3a", + "#b5546a" + ], "diverging": { "stops": [ "#2f6b9a", diff --git a/packages/flint-js/src/core/theme/types.ts b/packages/flint-js/src/core/theme/types.ts index b22250d6..32eacb58 100644 --- a/packages/flint-js/src/core/theme/types.ts +++ b/packages/flint-js/src/core/theme/types.ts @@ -107,6 +107,21 @@ export interface ThemeInk { series?: { single?: string; categorical?: string[]; + /** + * A larger indexed set the house reaches for when a chart has more + * series than its core {@link categorical} palette can name, in the + * spirit of Tableau's 10→20 step. The core set carries the house's + * identity at low cardinality (a handful of well-known inks); the + * extended set trades a little of that identity for the capacity to + * keep every series distinct up to its length. Grounding picks the + * smallest set whose length covers the series count; past the extended + * set's length the {@link overflow} ink takes the tail. + * + * Must contain the core set's inks as a prefix is *not* required — but + * ordering the shared hues first keeps a chart's colours stable as it + * grows. Unset ⇒ the house has only its core palette. + */ + categoricalExtended?: string[]; overflow?: string; sequential?: Ramp; diverging?: Ramp; @@ -531,6 +546,15 @@ export interface ResolvedSeriesInk { * the count. */ exhausted?: boolean; + /** + * More series than even the extended palette holds, but the house *does* + * name an {@link overflow} ink. The top {@link categorical}.length series + * by prominence take the indexed inks; every remaining ("other") series + * takes the one overflow ink. Realization orders the colour domain by + * share so it is the *smallest* series that fold into the overflow tail, + * not an arbitrary slice of the domain. + */ + overflowTail?: boolean; ramp?: Ramp; status?: { positive?: string; negative?: string; neutral?: string }; /** Concrete range to hand a continuous colour scale (already sampled). */ diff --git a/packages/flint-js/src/vegalite/theme.ts b/packages/flint-js/src/vegalite/theme.ts index 0cd97152..362b0a93 100644 --- a/packages/flint-js/src/vegalite/theme.ts +++ b/packages/flint-js/src/vegalite/theme.ts @@ -1540,6 +1540,46 @@ const POSITIVE_WORDS = /\b(above|increase|growth|gain|gains|positive|up|rise|sur * So pair each domain value with a role: by what it calls itself where the * label says so, and otherwise by the sign of the quantity it carries. */ +/** + * Order a colour domain by each category's share of the measure, largest + * first. Used when there are more series than the house's indexed set: the top + * inks go to the biggest categories and the small ones fold into the overflow + * tail, so the grey is the long tail rather than an arbitrary slice. Returns + * `undefined` for a continuous colour field or when no measure can be found. + */ +function orderDomainByShare(enc: any, node: any, table: any[]): any[] | undefined { + if (isContinuousColor(enc)) return undefined; + const field = enc.field; + if (!field) return undefined; + const domain: any[] = Array.isArray(enc.scale?.domain) && enc.scale.domain.length + ? enc.scale.domain.slice() + : [...new Set((table ?? []).map((r) => r?.[field]).filter((v) => v != null))]; + if (domain.length < 2) return undefined; + + // The measure whose share ranks the categories: theta for a pie, else the + // quantitative position (size before the axes for a bubble legend). + let measure: string | undefined; + for (const ch of ['theta', 'size', 'y', 'x'] as const) { + const e = node?.encoding?.[ch]; + if (e?.field && (e.type === 'quantitative' || e.type == null)) { measure = e.field; break; } + } + if (!measure) return undefined; + + const total = new Map(); + for (const row of table ?? []) { + const key = row?.[field]; + if (key == null) continue; + const v = Number(row?.[measure]); + total.set(key, (total.get(key) ?? 0) + (Number.isFinite(v) ? Math.abs(v) : 0)); + } + if (total.size === 0) return undefined; + // Stable descending sort, preserving the original domain order among ties. + return domain + .map((value, i) => ({ value, i, w: total.get(value) ?? 0 })) + .sort((a, b) => (b.w - a.w) || (a.i - b.i)) + .map((d) => d.value); +} + function statusRange( enc: any, node: any, @@ -1660,6 +1700,30 @@ function applySeriesInk(spec: any, d: DesignDecisions, table: any[], say: (p: st } continue; } + if (s.overflowTail && s.overflow && need > s.categorical.length) { + // Order the colour domain by share so the largest series keep + // a named ink and the small ones fold into the single overflow + // tail — a chart with a handful of headline categories and a + // grey remainder, not a wheel of near-identical hues. + const ordered = orderDomainByShare(enc, node, table); + if (ordered && ordered.length > s.categorical.length) { + const range = ordered.map((_, i) => + i < s.categorical.length ? s.categorical[i] : s.overflow!); + setColorRange(enc, range, { domain: ordered }); + // A legend that lists twenty identical grey rows is noise. + // Name the top inks; the grey wedges read as "everything + // else" without a row apiece. + if (enc.legend !== null) { + enc.legend = { ...(enc.legend ?? {}), values: ordered.slice(0, s.categorical.length) }; + } + if (!saidExhausted) { + say('ink.series.categorical', + `${need} series past ${s.categorical.length} inks — the ${s.categorical.length} largest keep a colour, the rest share one "other" ink; the legend names only the coloured ones`); + saidExhausted = true; + } + continue; + } + } setColorRange(enc, palette(need)); } }); From 4d493a7b87bd79cfc4a33c62b8a613e788cb470b Mon Sep 17 00:00:00 2001 From: Chenglong Wang Date: Fri, 31 Jul 2026 10:55:59 -0700 Subject: [PATCH 050/164] Others(N) overflow legend + universal overflow inks + labs colour panel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When discrete colour runs out, name the folded tail with an explicit "Others (N)" legend row in the muted overflow ink instead of hiding it. The realizer folds the tail via a Vega-Lite-native derived `__flintColorKey` (a `calculate` transform), re-points the colour encoding to it, and sets scale.domain = [...topK, "Others (N)"] / range = [...colours, overflow]. Marks keep their own rows (a pie still draws N wedges) — only the colour key each row shows is folded, so the legend dedupes to top-K + one Others. Fold only for independent marks. `subtreeHasConnectedMark` gates the fold off for line/area/trail, where a shared colour key would thread one grey scribble through unrelated series; those cycle the palette instead (top inks kept, the rest sharing the overflow ink but each still its own line). Fixes line-8 economist/mckinsey scribble. Universal overflow inks. economist/datawrapper/powerbi/powerbi-light/mckinsey previously leaked to Vega-Lite's default tableau scheme past their core palette; each now declares an `overflow` ink so the degradation is on-brand. datawrapper/powerbi/powerbi-light also gain a 12-colour categoricalExtended tier (Power BI's classic dashboard palette; datawrapper's richer qualitative set), so 7-12 series stay fully distinct and only >12 folds. Labs panel. New ColorDecisionFigure under /playground/labs: chart-type tabs (bar/scatter/pie), a category-count slider, and a colour-scheme selector (discrete house palettes vs a continuous viridis ramp) to inspect the decision — discrete folds to top-K + "Others (N)", continuous accommodates all N along the ramp with a standard legend. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 33e4b998-4b58-4d7e-9cf8-c107b10e45c2 --- .../src/core/theme/presets/datawrapper.ts | 17 +- .../src/core/theme/presets/economist.ts | 1 + .../src/core/theme/presets/mckinsey.ts | 3 +- .../src/core/theme/presets/powerbi-light.ts | 17 +- .../src/core/theme/presets/powerbi.ts | 17 +- packages/flint-js/src/vegalite/theme.ts | 87 ++++- site/src/playground/ColorDecisionFigure.tsx | 298 ++++++++++++++++++ site/src/playground/Labs.tsx | 2 + 8 files changed, 429 insertions(+), 13 deletions(-) create mode 100644 site/src/playground/ColorDecisionFigure.tsx diff --git a/packages/flint-js/src/core/theme/presets/datawrapper.ts b/packages/flint-js/src/core/theme/presets/datawrapper.ts index 4dcdced8..e283f975 100644 --- a/packages/flint-js/src/core/theme/presets/datawrapper.ts +++ b/packages/flint-js/src/core/theme/presets/datawrapper.ts @@ -44,6 +44,20 @@ export const datawrapper: ThemePreset = { "#2d8659", "#7e5aa2" ], + "categoricalExtended": [ + "#18a1cd", + "#e2a233", + "#c04a4a", + "#2d8659", + "#7e5aa2", + "#d97b4f", + "#5b8fb0", + "#b5546a", + "#8c9a3f", + "#c98ac0", + "#6b8e8a", + "#a67c52" + ], "sequential": { "stops": [ "#dceef6", @@ -70,7 +84,8 @@ export const datawrapper: ThemePreset = { "consumption": "quantize", "quantizeCount": 5 }, - "selection": {} + "selection": {}, + "overflow": "#b9bcbe" }, "accent": "#18a1cd" }, diff --git a/packages/flint-js/src/core/theme/presets/economist.ts b/packages/flint-js/src/core/theme/presets/economist.ts index c4df2e59..af44b0a4 100644 --- a/packages/flint-js/src/core/theme/presets/economist.ts +++ b/packages/flint-js/src/core/theme/presets/economist.ts @@ -62,6 +62,7 @@ export const economist: ThemePreset = { "negative": "#e3120b", "neutral": "#b8c4cc" }, + "overflow": "#b0aca1", "selection": { "signed": "status", "statusUse": "anySigned" diff --git a/packages/flint-js/src/core/theme/presets/mckinsey.ts b/packages/flint-js/src/core/theme/presets/mckinsey.ts index 49c6c7d3..749dd202 100644 --- a/packages/flint-js/src/core/theme/presets/mckinsey.ts +++ b/packages/flint-js/src/core/theme/presets/mckinsey.ts @@ -78,7 +78,8 @@ export const mckinsey: ThemePreset = { "selection": { "partToWhole": "sequentialRamp", "signed": "diverging" - } + }, + "overflow": "#b6bfc7" }, "accent": "#2251ff" }, diff --git a/packages/flint-js/src/core/theme/presets/powerbi-light.ts b/packages/flint-js/src/core/theme/presets/powerbi-light.ts index d0099821..869564ed 100644 --- a/packages/flint-js/src/core/theme/presets/powerbi-light.ts +++ b/packages/flint-js/src/core/theme/presets/powerbi-light.ts @@ -51,6 +51,20 @@ export const powerbiLight: ThemePreset = { "#e044a7", "#744ec2" ], + "categoricalExtended": [ + "#118dff", + "#12239e", + "#e66c37", + "#6b007b", + "#e044a7", + "#744ec2", + "#d9b300", + "#d64550", + "#197278", + "#5c2e91", + "#ff9d3b", + "#4a9c2d" + ], "diverging": { "stops": [ "#118dff", @@ -73,7 +87,8 @@ export const powerbiLight: ThemePreset = { "signed": "diverging", "statusUse": "thresholdOnly", "redundantWithFacet": "single" - } + }, + "overflow": "#bcbcbc" }, "accent": "#118dff" }, diff --git a/packages/flint-js/src/core/theme/presets/powerbi.ts b/packages/flint-js/src/core/theme/presets/powerbi.ts index a5e90798..72def375 100644 --- a/packages/flint-js/src/core/theme/presets/powerbi.ts +++ b/packages/flint-js/src/core/theme/presets/powerbi.ts @@ -47,6 +47,20 @@ export const powerbi: ThemePreset = { "#e044a7", "#744ec2" ], + "categoricalExtended": [ + "#118dff", + "#12239e", + "#e66c37", + "#6b007b", + "#e044a7", + "#744ec2", + "#d9b300", + "#d64550", + "#197278", + "#5c2e91", + "#ff9d3b", + "#4a9c2d" + ], "diverging": { "stops": [ "#118dff", @@ -69,7 +83,8 @@ export const powerbi: ThemePreset = { "signed": "diverging", "statusUse": "thresholdOnly", "redundantWithFacet": "single" - } + }, + "overflow": "#8a8886" }, "accent": "#118dff" }, diff --git a/packages/flint-js/src/vegalite/theme.ts b/packages/flint-js/src/vegalite/theme.ts index 362b0a93..adc99bc2 100644 --- a/packages/flint-js/src/vegalite/theme.ts +++ b/packages/flint-js/src/vegalite/theme.ts @@ -51,6 +51,20 @@ export function collectMarkTypes(spec: any): string[] { return [...found]; } +/** Series marks that draw a connected path — folding several onto one colour + * key would thread a single line through unrelated readings. */ +const CONNECTED_MARKS = new Set(['line', 'area', 'trail']); + +/** True when any mark at or under this node draws a connected series path. */ +function subtreeHasConnectedMark(node: any): boolean { + let found = false; + walk(node, (n) => { + const t = markTypeOf(n.mark); + if (t && CONNECTED_MARKS.has(t)) found = true; + }); + return found; +} + /** * What the spec actually put on the two screen axes. A template is free to * name its semantic channels `high`/`low`/`open`/`close`; the reader still @@ -1540,6 +1554,35 @@ const POSITIVE_WORDS = /\b(above|increase|growth|gain|gains|positive|up|rise|sur * So pair each domain value with a role: by what it calls itself where the * label says so, and otherwise by the sign of the quantity it carries. */ +/** + * The synthetic field name a colour-overflow chart keys off: the top-share + * categories keep their own name, the tail collapses onto one "Others (N)" + * value so the legend can name it. + */ +const OVERFLOW_KEY_FIELD = '__flintColorKey'; + +/** + * Fold a colour field's tail categories onto one key via a Vega-Lite + * `calculate` transform: `topK` values pass through, everything else becomes + * `othersLabel`. The transform is prepended so the derived field exists before + * any the template added. Marks keep their own rows — only the colour key each + * row carries is remapped. + */ +function addColorKeyTransform( + node: any, + field: string, + topK: any[], + othersLabel: string, + keyField: string, +): void { + const f = JSON.stringify(field); + const calc = `indexof(${JSON.stringify(topK)}, datum[${f}]) >= 0 ? datum[${f}] : ${JSON.stringify(othersLabel)}`; + const transform = Array.isArray(node.transform) ? node.transform.slice() : []; + if (transform.some((t: any) => t?.as === keyField)) return; + transform.push({ calculate: calc, as: keyField }); + node.transform = transform; +} + /** * Order a colour domain by each category's share of the measure, largest * first. Used when there are more series than the house's indexed set: the top @@ -1700,25 +1743,51 @@ function applySeriesInk(spec: any, d: DesignDecisions, table: any[], say: (p: st } continue; } - if (s.overflowTail && s.overflow && need > s.categorical.length) { + if (s.overflowTail && s.overflow && need > s.categorical.length && !subtreeHasConnectedMark(node)) { // Order the colour domain by share so the largest series keep // a named ink and the small ones fold into the single overflow // tail — a chart with a handful of headline categories and a // grey remainder, not a wheel of near-identical hues. + // + // Only for marks that stand on their own — a pie's wedges, a + // scatter's points, a bar's bars. A line or an area is a + // *connected* series: fold two of them onto one colour key and + // Vega-Lite threads a single path through both, a grey scribble + // where two readings should be. Those keep the palette (top + // inks, the rest sharing the overflow ink but each still its + // own line) via the fall-through below. const ordered = orderDomainByShare(enc, node, table); if (ordered && ordered.length > s.categorical.length) { - const range = ordered.map((_, i) => - i < s.categorical.length ? s.categorical[i] : s.overflow!); - setColorRange(enc, range, { domain: ordered }); - // A legend that lists twenty identical grey rows is noise. - // Name the top inks; the grey wedges read as "everything - // else" without a row apiece. + const k = s.categorical.length; + const topK = ordered.slice(0, k); + const restCount = ordered.length - k; + // One legend row stands in for the grey tail — an explicit + // "Others (N)" swatch in the overflow ink, so the reader is + // told how many categories share it rather than being left + // to infer that the grey wedges are "everything else". + const othersLabel = `Others (${restCount})`; + const field = enc.field; + const keyField = OVERFLOW_KEY_FIELD; + // A derived key folds the tail categories onto one value. + // The marks keep their own rows (a pie still draws N wedges, + // a bar N bars) — only which colour-key each row shows is + // remapped, so the legend dedupes to top-K plus one Others. + addColorKeyTransform(node, field, topK, othersLabel, keyField); + enc.field = keyField; + enc.type = 'nominal'; + const domain = [...topK, othersLabel]; + const range = [...s.categorical.slice(0, k), s.overflow!]; + setColorRange(enc, range, { domain }); if (enc.legend !== null) { - enc.legend = { ...(enc.legend ?? {}), values: ordered.slice(0, s.categorical.length) }; + // The K+1 domain is short enough to list in full; drop + // any values pin the template left so Others shows too. + const legend = { ...(enc.legend ?? {}) }; + delete legend.values; + enc.legend = legend; } if (!saidExhausted) { say('ink.series.categorical', - `${need} series past ${s.categorical.length} inks — the ${s.categorical.length} largest keep a colour, the rest share one "other" ink; the legend names only the coloured ones`); + `${need} series past ${k} inks — the ${k} largest keep a colour, the rest fold into one "${othersLabel}" ink named in the legend`); saidExhausted = true; } continue; diff --git a/site/src/playground/ColorDecisionFigure.tsx b/site/src/playground/ColorDecisionFigure.tsx new file mode 100644 index 00000000..3b7388e3 --- /dev/null +++ b/site/src/playground/ColorDecisionFigure.tsx @@ -0,0 +1,298 @@ +import { useMemo, useState, type CSSProperties } from 'react'; +import { assembleVegaLite, THEME_PRESETS, type ChartAssemblyInput } from 'flint-chart'; +import { VegaLiteView } from '../components/VegaLiteView'; +import { siteTheme } from '../shared/theme'; + +/** + * Interactive dev-labs panel for the categorical colour-overflow decision. + * + * Pick a chart type, drag the category count up, and switch colour schemes to + * watch how Flint hands out inks when the field has more categories than a + * house owns colours: + * + * - a *discrete* house palette keeps its indexed set (auto-upsized to the + * house's extended tier where it has one), then folds the long tail onto one + * muted overflow ink with an explicit "Others (N)" legend row — the top + * categories by share stay named, the rest read as "everything else"; + * - a *continuous* ramp samples every category along the scheme, so all N are + * accommodated and the legend is the standard categorical list. + * + * The decision lives in the Vega-Lite theme realizer, so this panel drives that + * backend directly rather than the multi-backend adapter. + */ + +/** Deterministic RNG so the demo data is stable across re-renders. */ +function mulberry32(seed: number): () => number { + let a = seed >>> 0; + return () => { + a |= 0; + a = (a + 0x6d2b79f5) | 0; + let t = Math.imul(a ^ (a >>> 15), 1 | a); + t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; + return ((t ^ (t >>> 14)) >>> 0) / 4294967296; + }; +} + +type ChartKind = 'Bar Chart' | 'Scatter Plot' | 'Pie Chart'; + +const CHART_KINDS: ChartKind[] = ['Bar Chart', 'Scatter Plot', 'Pie Chart']; + +/** N categories, each with a value (skewed so a share ordering is meaningful) + * and an X/Y position for the scatter case. */ +function categoryRows(n: number, kind: ChartKind): Record[] { + const rng = mulberry32(0x51c0 + n); + return Array.from({ length: n }, (_, i) => { + const Category = `Cat ${String(i + 1).padStart(2, '0')}`; + if (kind === 'Scatter Plot') { + return { Category, X: Math.round(rng() * 100), Y: Math.round(rng() * 100) }; + } + // A gentle decreasing skew so the earliest categories are the biggest — + // gives the "top by share keep a colour" fold something to rank. + return { Category, Value: Math.round(12 + (n - i) * (3 + rng() * 5)) }; + }); +} + +const BASE = { width: 520, height: 400 } as const; + +function buildInput(kind: ChartKind, n: number): ChartAssemblyInput { + const values = categoryRows(n, kind); + if (kind === 'Scatter Plot') { + return { + data: { values }, + semantic_types: { Category: 'Category', X: 'Quantity', Y: 'Quantity' }, + chart_spec: { + chartType: kind, + encodings: { x: { field: 'X' }, y: { field: 'Y' }, color: { field: 'Category' } }, + baseSize: BASE, + }, + }; + } + if (kind === 'Pie Chart') { + return { + data: { values }, + semantic_types: { Category: 'Category', Value: 'Quantity' }, + chart_spec: { + chartType: kind, + encodings: { size: { field: 'Value' }, color: { field: 'Category' } }, + baseSize: BASE, + }, + }; + } + return { + data: { values }, + semantic_types: { Category: 'Category', Value: 'Quantity' }, + chart_spec: { + chartType: kind, + encodings: { x: { field: 'Category' }, y: { field: 'Value' }, color: { field: 'Category' } }, + baseSize: BASE, + }, + }; +} + +/** House themes carry the categorical set + extended tier + overflow ink that + * the fold reads; "continuous" is a synthetic option handled separately. */ +const CONTINUOUS = 'continuous'; +type SchemeId = string; + +const SCHEMES: { id: SchemeId; label: string }[] = [ + { id: 'nyt', label: 'NYT — discrete (5 → 12 + Others)' }, + { id: 'nature', label: 'Nature — discrete (6 → 12 + Others)' }, + { id: 'datawrapper', label: 'Datawrapper — discrete (5 → 12 + Others)' }, + { id: 'powerbi-light', label: 'Power BI Light — discrete (6 → 12 + Others)' }, + { id: 'economist', label: 'Economist — discrete, restrained (6 + Others)' }, + { id: CONTINUOUS, label: 'Continuous ramp (viridis) — accommodate all' }, +]; + +/** Walk the assembled spec and point every categorical colour channel at a + * continuous scheme, so a nominal field is sampled across the whole ramp. */ +function applyContinuousScheme(spec: any, ramp: string): void { + const visit = (node: any) => { + if (!node || typeof node !== 'object') return; + for (const channel of ['color', 'fill', 'stroke']) { + const enc = node.encoding?.[channel]; + if (enc && enc.field) { + enc.scale = { scheme: ramp }; + if (enc.legend && typeof enc.legend === 'object') delete enc.legend.values; + } + } + for (const key of Object.keys(node)) { + const v = node[key]; + if (Array.isArray(v)) v.forEach(visit); + else if (v && typeof v === 'object') visit(v); + } + }; + visit(spec); +} + +const labelStyle: CSSProperties = { fontSize: 13, color: siteTheme.textMuted, fontWeight: 500 }; + +const pill = (active: boolean): CSSProperties => ({ + cursor: 'pointer', + fontSize: 12, + padding: '5px 12px', + borderRadius: 7, + border: 'none', + whiteSpace: 'nowrap', + background: active ? siteTheme.surface : 'transparent', + boxShadow: active ? '0 1px 2px rgba(0,0,0,0.12)' : 'none', + color: active ? siteTheme.text : siteTheme.textMuted, + fontWeight: active ? 600 : 500, +}); + +export function ColorDecisionFigure() { + const [kind, setKind] = useState('Pie Chart'); + const [count, setCount] = useState(18); + const [scheme, setScheme] = useState('nyt'); + + const { spec, error, folded } = useMemo(() => { + try { + const input = buildInput(kind, count); + if (scheme === CONTINUOUS) { + const s = assembleVegaLite(input) as any; + applyContinuousScheme(s, 'viridis'); + return { spec: s, error: null as string | null, folded: false }; + } + const preset = THEME_PRESETS[scheme]; + const s = assembleVegaLite({ ...input, theme_spec: preset.spec }) as any; + // The realizer adds a `__flintColorKey` transform only when it folds the + // tail — a cheap signal for the caption. + const foldedNow = JSON.stringify(s).includes('__flintColorKey'); + return { spec: s, error: null, folded: foldedNow }; + } catch (err) { + return { spec: null, error: String((err as Error)?.message ?? err), folded: false }; + } + }, [kind, count, scheme]); + + const pct = ((count - 3) / (40 - 3)) * 100; + + return ( +
+
+

+ Colour overflow — top-K + "Others (N)" +

+

+ Add categories past a house's palette and watch the decision. A discrete house + keeps its indexed inks (auto-upsized to its extended tier), then folds the long tail onto one muted + overflow ink with an explicit Others (N) legend row. A continuous ramp + samples every category, so all N are accommodated with a standard legend. Lines and areas never fold + (that would thread one path through unrelated series), so this panel uses bars, points, and wedges. +

+
+ +
+
+ {/* Chart-type tabs */} +
+ {CHART_KINDS.map((id) => ( + + ))} +
+ + {/* Scheme selector */} + + + {/* Category-count slider */} + + + + {scheme === CONTINUOUS + ? 'all categories sampled along the ramp' + : folded + ? 'folded — top inks kept, tail → Others' + : 'within palette — every category named'} + +
+ + {/* Chart stage */} +
+ {error ? ( +
{error}
+ ) : ( + spec && + )} +
+
+
+ ); +} diff --git a/site/src/playground/Labs.tsx b/site/src/playground/Labs.tsx index 7b66debb..db9bfc62 100644 --- a/site/src/playground/Labs.tsx +++ b/site/src/playground/Labs.tsx @@ -1,6 +1,7 @@ import { DodgeToggleFigure } from './DodgeToggleFigure'; import { LocalDodgeFigure } from './LocalDodgeFigure'; import { BandExpansionFigure } from './BandExpansionFigure'; +import { ColorDecisionFigure } from './ColorDecisionFigure'; export function Labs() { return ( @@ -8,6 +9,7 @@ export function Labs() {

Labs

+ From 7ca968c4ff1279e3c817e64f506027fba1dee2d9 Mon Sep 17 00:00:00 2001 From: Chenglong Wang Date: Fri, 31 Jul 2026 11:28:15 -0700 Subject: [PATCH 051/164] theme: merge pie overflow tail into one slice; fix hub spikes and flat y-title vs top legend Three fixes to the colour-overflow / pie path, all verified across the r2 corpus (85/85) and theme-audit (62/62): 1. Pie "Others" aggregation. On a part-to-whole (arc) mark the overflow tail is a *share*, so summing it is meaningful: the folded "Others (N)" categories now collapse into a single summed slice at the data level (a `sum` aggregate grouped by the derived colour key) instead of fanning out N same-grey wedges. Gated to a lone arc view (no layer/facet/concat, so a sibling value-label layer can't desync) with a plain summed theta field and no other field-bound encoding the aggregate would drop. 2. Pie hub spikes. Wedges taper to a point at the centre; a mitred white rule on those acute tips shot a spike past the hub (worse the thinner the slice). The slice-gap stroke now uses a round join so the rule stays a rule all the way to the middle. 3. NYT flat y-title vs top legend. A vertical-axis title laid flat sits at the plot's top-left corner, and a multi-row top legend landed its last row ("Others (38)") in that same strip. The top key is now pushed up off the plot far enough to clear the flat title. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 33e4b998-4b58-4d7e-9cf8-c107b10e45c2 --- packages/flint-js/src/vegalite/theme.ts | 87 ++++++++++++++++++++++++- 1 file changed, 85 insertions(+), 2 deletions(-) diff --git a/packages/flint-js/src/vegalite/theme.ts b/packages/flint-js/src/vegalite/theme.ts index adc99bc2..4ac48aed 100644 --- a/packages/flint-js/src/vegalite/theme.ts +++ b/packages/flint-js/src/vegalite/theme.ts @@ -65,6 +65,47 @@ function subtreeHasConnectedMark(node: any): boolean { return found; } +/** Part-to-whole marks whose slices are a share of a summed measure — the + * overflow tail can be summed into a single "Others" slice at the data level. */ +const PART_TO_WHOLE_MARKS = new Set(['arc']); + +/** True when the node's own mark is a part-to-whole (share) mark. */ +function isPartToWholeMark(node: any): boolean { + const t = markTypeOf(node.mark); + return !!t && PART_TO_WHOLE_MARKS.has(t); +} + +/** + * Every field the node's encodings read, minus the colour and angle fields the + * overflow fold already accounts for. If anything is left, a sum-aggregate + * would drop it, so the tail is kept as separate wedges rather than merged. + */ +function otherEncodedFields(encoding: any, keep: Set): string[] { + const out: string[] = []; + for (const ch of Object.keys(encoding ?? {})) { + const e = (encoding as any)[ch]; + const list = Array.isArray(e) ? e : [e]; + for (const one of list) { + const f = one?.field; + if (typeof f === 'string' && !keep.has(f)) out.push(f); + } + } + return out; +} + +/** + * Sum the angle measure across each colour key. On a part-to-whole chart the + * top categories keep their own key (one row each, unchanged) and the folded + * tail — all sharing the "Others (N)" key — collapse into a single summed + * slice. Prepended after the key `calculate` so the key exists to group on. + */ +function addTailAggregate(node: any, thetaField: string, keyField: string): void { + const transform = Array.isArray(node.transform) ? node.transform.slice() : []; + if (transform.some((t: any) => Array.isArray(t?.aggregate) && (t?.groupby ?? []).includes(keyField))) return; + transform.push({ aggregate: [{ op: 'sum', field: thetaField, as: thetaField }], groupby: [keyField] }); + node.transform = transform; +} + /** * What the spec actually put on the two screen axes. A template is free to * name its semantic channels `high`/`low`/`open`/`close`; the reader still @@ -1333,6 +1374,10 @@ function applySliceGap( } else { mark.stroke = slice.color; mark.strokeWidth = slice.gap; + // Wedges taper to a point at the hub; a mitred stroke on those acute + // tips shoots a spike past the centre (worse the thinner the slice). + // Round the join so the rule stays a rule all the way to the middle. + mark.strokeJoin = 'round'; if (!said) { say('marks.slice.gap', `a ${slice.gap}px rule cuts the wedges apart — two arcs of the same size read as two shapes, not one`); @@ -1673,7 +1718,12 @@ function applySeriesInk(spec: any, d: DesignDecisions, table: any[], say: (p: st let saidCollapse = false; let saidStatus = false; let saidExhausted = false; - + // Merging the overflow tail into one summed slice rewrites the data feeding + // this node. Safe for a lone arc view; inside a layer/facet/concat the + // sibling layers (a value-label text mark, say) still read the full rows, + // so summing one layer's data would desync them. Only merge when the spec + // is a single view. + const composed = !!(spec?.layer || spec?.concat || spec?.hconcat || spec?.vconcat || spec?.facet || spec?.spec); /** The house's indexed set, extended to `n` positions with the overflow ink. */ const palette = (n: number): string[] => { const out = s.categorical.slice(0, n); @@ -1785,9 +1835,30 @@ function applySeriesInk(spec: any, d: DesignDecisions, table: any[], say: (p: st delete legend.values; enc.legend = legend; } + // On a part-to-whole chart the tail is a *share*, so summing + // it is meaningful: merge the "Others" categories into one + // slice at the data level rather than fanning out N same-grey + // wedges. Only when the angle is a plain summed field (a + // count already groups by the colour key) and nothing else is + // encoded off a field the aggregate would drop. + const thetaEnc = node.encoding?.theta; + const thetaField = thetaEnc?.field; + let merged = false; + if ( + isPartToWholeMark(node) && + !composed && + typeof thetaField === 'string' && + !thetaEnc.aggregate && + otherEncodedFields(node.encoding, new Set([thetaField, keyField])).length === 0 + ) { + addTailAggregate(node, thetaField, keyField); + merged = true; + } if (!saidExhausted) { say('ink.series.categorical', - `${need} series past ${k} inks — the ${k} largest keep a colour, the rest fold into one "${othersLabel}" ink named in the legend`); + merged + ? `${need} series past ${k} inks — the ${k} largest keep a colour, the rest sum into one "${othersLabel}" slice` + : `${need} series past ${k} inks — the ${k} largest keep a colour, the rest fold into one "${othersLabel}" ink named in the legend`); saidExhausted = true; } continue; @@ -2012,6 +2083,18 @@ function applyLegend(spec: any, config: any, d: DesignDecisions, table: any[], s } } if (!l.title) config.legend.title = null; + // A vertical-axis title laid flat sits at the plot's top-left corner; a key + // riding along the top of that same plot lands its last row in the very + // strip the title occupies, and on a multi-row key the two collide. Push + // the key up off the plot far enough to clear the flat title. + const yTitleForOffset = d.axes?.y?.title; + if (l.orient === 'top' && yTitleForOffset?.show + && (yTitleForOffset.placement === 'flatAboveAxis' || yTitleForOffset.placement === 'inline')) { + const clearance = (yTitleForOffset.fontSize ?? 11) + 12; + config.legend.offset = (config.legend.offset ?? 18) + clearance; + say('legend.offset', + `the top key clears the flat axis title by ${clearance}px so its last row does not land on the title`); + } // A top or bottom key is a caption to the whole graphic, so it begins where // the graphic does — flush with the title down the left edge — not indented // to the plot rectangle the way Vega-Lite lays it by default. `bounds: From 5df4771d5d7b3c1b1ca12ad69426a0cd0f9ca246 Mon Sep 17 00:00:00 2001 From: Chenglong Wang Date: Fri, 31 Jul 2026 11:48:23 -0700 Subject: [PATCH 052/164] theme: size a folded colour legend for its K+1 keys, not the field's full count MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `assemble` shrinks a nominal colour key's labels to 8px (and its swatches) once the field's distinct count reaches 16, so all the names fit. When a theme then folds the key to the top-K largest plus one "Others (N)" row, only ~K+1 keys actually render, yet they stayed frozen at the 8px meant for the full list — needlessly tiny. The overflow fold now recomputes that shrink against what it will draw: when the folded list (K+1) is below the high-cardinality threshold, the label/swatch overrides are dropped so the keys stand at the house's own size. To keep the restored (wider) labels from tripping the wrap pass into truncating the list — which would hide the very "Others (N)" row the fold built — `wrapWideKeys` no longer caps an already-folded key (identified by the derived `__flintColorKey`): it wraps into columns but never truncates. Verified on nyt scatter-color-n50: the legend now shows all 12 series plus "Others (38)" at the house label size instead of a squeezed "…2 entries". Gates: flint+site tsc, 694 vitest, site vite build, r2 85/0, theme-audit 62/0. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 33e4b998-4b58-4d7e-9cf8-c107b10e45c2 --- packages/flint-js/src/vegalite/assemble.ts | 4 ++++ packages/flint-js/src/vegalite/theme.ts | 28 +++++++++++++++++++++- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/packages/flint-js/src/vegalite/assemble.ts b/packages/flint-js/src/vegalite/assemble.ts index 8b2dc375..7183fce2 100644 --- a/packages/flint-js/src/vegalite/assemble.ts +++ b/packages/flint-js/src/vegalite/assemble.ts @@ -1004,6 +1004,10 @@ function buildVLEncodings( // Legend sizing for high-cardinality nominal color/group if (encodingObj.type === "nominal" && (channel === 'color' || channel === 'group')) { const actualDomain = [...new Set(data.map(r => r[fieldName]))]; + // Threshold kept in sync with HIGH_CARDINALITY_LEGEND_MIN in + // vegalite/theme.ts: when a theme later folds the key to a short + // top-K + Others list, that pass recomputes this shrink against + // the folded count so short legends are not squeezed to 8px. if (actualDomain.length >= 16) { if (!encodingObj.legend) encodingObj.legend = {}; encodingObj.legend.symbolSize = 12; diff --git a/packages/flint-js/src/vegalite/theme.ts b/packages/flint-js/src/vegalite/theme.ts index 4ac48aed..fe7a92da 100644 --- a/packages/flint-js/src/vegalite/theme.ts +++ b/packages/flint-js/src/vegalite/theme.ts @@ -69,6 +69,16 @@ function subtreeHasConnectedMark(node: any): boolean { * overflow tail can be summed into a single "Others" slice at the data level. */ const PART_TO_WHOLE_MARKS = new Set(['arc']); +/** + * The distinct-count at which `assemble` shrinks a colour key's labels to fit + * the field's full cardinality (see `assemble.ts`, "Legend sizing for + * high-cardinality nominal color/group"). When the overflow fold collapses the + * key to a short top-K + Others list, only a handful of rows render, so that + * shrink is recomputed against the folded count and short lists are handed back + * to the house's own size. Kept in sync with the literal in `assemble.ts`. + */ +const HIGH_CARDINALITY_LEGEND_MIN = 16; + /** True when the node's own mark is a part-to-whole (share) mark. */ function isPartToWholeMark(node: any): boolean { const t = markTypeOf(node.mark); @@ -1833,6 +1843,17 @@ function applySeriesInk(spec: any, d: DesignDecisions, table: any[], say: (p: st // any values pin the template left so Others shows too. const legend = { ...(enc.legend ?? {}) }; delete legend.values; + // assemble shrank these labels (and their swatches) to + // fit the field's *full* cardinality; the fold now shows + // only the K largest plus one Others row, so that shrink + // is measured against the wrong count. Recompute it here + // against what actually renders — a short list stands at + // the house's own size, not squeezed to 8px for names it + // no longer draws. + if (topK.length + 1 < HIGH_CARDINALITY_LEGEND_MIN) { + delete legend.labelFontSize; + delete legend.symbolSize; + } enc.legend = legend; } // On a part-to-whole chart the tail is a *share*, so summing @@ -2240,8 +2261,13 @@ function wrapWideKeys( const columns = Math.max(1, Math.floor(block / entryWidth)); if (entries.length <= columns) continue; enc.legend = { ...(enc.legend ?? {}), columns }; + // A folded key is already the bounded top-K + "Others (N)" list the + // overflow pass built to be listable in full; capping it would hide + // the very Others row that stands in for the tail. Wrap it into + // columns, but never truncate it. + const folded = enc.field === OVERFLOW_KEY_FIELD; const cap = columns * MAX_ROWS; - if (entries.length > cap) enc.legend.symbolLimit = cap; + if (!folded && entries.length > cap) enc.legend.symbolLimit = cap; say('legend.columns', `${entries.length} keys in one row overrun the ${Math.round(block)}px block — wrapped to ${columns} columns`); } From 8ee6119729924fbe19f0eb0ab013dd22da738728 Mon Sep 17 00:00:00 2001 From: Chenglong Wang Date: Fri, 31 Jul 2026 12:20:08 -0700 Subject: [PATCH 053/164] =?UTF-8?q?theme:=20complete=20colour=20spec=20?= =?UTF-8?q?=E2=80=94=20McKinsey=20extended=20palette,=20distinct=20part-to?= =?UTF-8?q?-whole,=20sequential=20ramps,=20pie-label=20gutter?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Colour completeness pass across the seven houses, driven by McKinsey's "weird" monochrome-blue pies and the too-narrow five-colour cap. McKinsey - Gave the house a categoricalExtended (10) that walks the cool wheel it actually uses — blue → cyan → teal → a restrained violet → slate — with the core five as a stable prefix. A ten-slice pie now names every vendor instead of folding eight into a grey "Others". - Switched partToWhole from sequentialRamp to categorical: a four-source donut read as four indistinguishable blue shades where every other house used distinct hues. Ordered-shade part-to-whole stays a schema option for houses that want it, gated to the sequential ramp's resolution. Sequential ramps - Added explicit sequential ramps to the five houses that lacked them (nyt/economist/nature/powerbi/powerbi-light) so heat maps and choropleths get real dynamic range instead of the two-stop monochrome fallback. Power BI's is dim→bright for its dark canvas; the light houses run light→deep. Pie value labels - An outside arc label sits at radius+14 and reaches out half its own width, so on a wedge grown to fill the plot box the widest numbers ran off the canvas and lost a digit. When a house prints numbers outside, pull the wedge in to reserve the label gutter and write that radius back onto the arc so the wedge and the label ring agree. Gates: flint+site tsc, 694 vitest, site vite build, r2 85/0, theme-audit 62/0. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 33e4b998-4b58-4d7e-9cf8-c107b10e45c2 --- packages/flint-js/src/core/theme/ground.ts | 23 ++++++++++++---- .../src/core/theme/presets/economist.ts | 14 ++++++++++ .../src/core/theme/presets/mckinsey.ts | 20 +++++++++++++- .../flint-js/src/core/theme/presets/nature.ts | 14 ++++++++++ .../flint-js/src/core/theme/presets/nyt.ts | 15 +++++++++++ .../src/core/theme/presets/powerbi-light.ts | 14 ++++++++++ .../src/core/theme/presets/powerbi.ts | 17 ++++++++++++ packages/flint-js/src/vegalite/theme.ts | 27 ++++++++++++++++++- 8 files changed, 137 insertions(+), 7 deletions(-) diff --git a/packages/flint-js/src/core/theme/ground.ts b/packages/flint-js/src/core/theme/ground.ts index 2e9a8cb1..9d638148 100644 --- a/packages/flint-js/src/core/theme/ground.ts +++ b/packages/flint-js/src/core/theme/ground.ts @@ -1442,11 +1442,24 @@ function groundSeriesInk( } if (signals.isPartToWhole && selection.partToWhole === 'sequentialRamp' && s.sequential?.stops?.length) { - // One ramp, consumed as an indexed set: the largest share takes the - // darkest end, so the ramp is sampled in reverse. - const ramp = offSurface(s.sequential, surfaceColour, say)!; - const range = sampleRamp(ramp.stops, Math.max(2, count)).reverse(); - return { ...base, mode: 'sequential', ramp, range }; + // A single-hue ramp names a part-to-whole cleanly only while the slices + // stay as few as the ramp has control points. Sampled past that, the + // adjacent shades blur into one another and the wheel reads as a smear + // of near-identical tints — worse than distinct hues, and it also skips + // the "Others" tail a large pie needs. Up to the ramp's resolution the + // house keeps its monochrome part-to-whole look; beyond it, fall through + // to the indexed set (distinct hues + an Others fold) so every slice + // stays nameable. + const rampResolution = s.sequential.stops.length; + if (!signals.seriesCountKnown || count <= rampResolution) { + // One ramp, consumed as an indexed set: the largest share takes the + // darkest end, so the ramp is sampled in reverse. + const ramp = offSurface(s.sequential, surfaceColour, say)!; + const range = sampleRamp(ramp.stops, Math.max(2, count)).reverse(); + return { ...base, mode: 'sequential', ramp, range }; + } + say('ink.series.selection.partToWhole', + `${count} slices exceed the ${rampResolution}-stop ramp's resolution — distinct hues name them better than shades, so the indexed set stands`); } if (signals.isSigned && s.status && selection.signed === 'status' && selection.statusUse !== 'never') { diff --git a/packages/flint-js/src/core/theme/presets/economist.ts b/packages/flint-js/src/core/theme/presets/economist.ts index af44b0a4..6300199b 100644 --- a/packages/flint-js/src/core/theme/presets/economist.ts +++ b/packages/flint-js/src/core/theme/presets/economist.ts @@ -44,6 +44,20 @@ export const economist: ThemePreset = { "#3ebcd2", "#c8b88a" ], + // Continuous measure: the Economist blue, light to deep. + "sequential": { + "stops": [ + "#dcebf2", + "#a7ccdd", + "#6ba7c6", + "#2f88ae", + "#006ba2", + "#003f5c" + ], + "space": "lab", + "endpointsAgainstSurface": true, + "consumption": "interpolate" + }, "diverging": { "stops": [ "#006ba2", diff --git a/packages/flint-js/src/core/theme/presets/mckinsey.ts b/packages/flint-js/src/core/theme/presets/mckinsey.ts index 749dd202..019ef6da 100644 --- a/packages/flint-js/src/core/theme/presets/mckinsey.ts +++ b/packages/flint-js/src/core/theme/presets/mckinsey.ts @@ -41,6 +41,24 @@ export const mckinsey: ThemePreset = { "#00cfb4", "#8c9ba5" ], + // The house is blue at heart, so the extended set does not + // reach for a rainbow — it walks the cool wheel the way + // McKinsey's own decks do: blue → cyan → teal, then a + // restrained turn into violet before the slate. The core five + // stay a prefix so a chart's colours don't reshuffle as it + // grows past the handful the identity is built on. + "categoricalExtended": [ + "#051c2c", + "#2251ff", + "#00a9f4", + "#00cfb4", + "#8c9ba5", + "#7c5cff", + "#0e7c8b", + "#6fb7e8", + "#b39ddb", + "#3d4f66" + ], "sequential": { "stops": [ "#eef3f8", @@ -76,7 +94,7 @@ export const mckinsey: ThemePreset = { "consumption": "interpolate" }, "selection": { - "partToWhole": "sequentialRamp", + "partToWhole": "categorical", "signed": "diverging" }, "overflow": "#b6bfc7" diff --git a/packages/flint-js/src/core/theme/presets/nature.ts b/packages/flint-js/src/core/theme/presets/nature.ts index 07a71b94..618ff535 100644 --- a/packages/flint-js/src/core/theme/presets/nature.ts +++ b/packages/flint-js/src/core/theme/presets/nature.ts @@ -56,6 +56,20 @@ export const nature: ThemePreset = { "#88ccee", "#999933" ], + // Continuous measure: the house blue (Wong), light to deep. + "sequential": { + "stops": [ + "#e6f0f7", + "#b3d3e8", + "#79b0d5", + "#3a8fc4", + "#0072b2", + "#00436a" + ], + "space": "lab", + "endpointsAgainstSurface": true, + "consumption": "interpolate" + }, "diverging": { "stops": [ "#0072b2", diff --git a/packages/flint-js/src/core/theme/presets/nyt.ts b/packages/flint-js/src/core/theme/presets/nyt.ts index ec50ccb4..a8bd189a 100644 --- a/packages/flint-js/src/core/theme/presets/nyt.ts +++ b/packages/flint-js/src/core/theme/presets/nyt.ts @@ -57,6 +57,21 @@ export const nyt: ThemePreset = { "#d07b3a", "#b5546a" ], + // Continuous measure: the house blue, light tint to deep, so + // a heat map or choropleth reads as one hue growing, not a set. + "sequential": { + "stops": [ + "#eef4f8", + "#c2d8e7", + "#8bb0cf", + "#5187b3", + "#2f6b9a", + "#1c4363" + ], + "space": "lab", + "endpointsAgainstSurface": true, + "consumption": "interpolate" + }, "diverging": { "stops": [ "#2f6b9a", diff --git a/packages/flint-js/src/core/theme/presets/powerbi-light.ts b/packages/flint-js/src/core/theme/presets/powerbi-light.ts index 869564ed..e5534ad6 100644 --- a/packages/flint-js/src/core/theme/presets/powerbi-light.ts +++ b/packages/flint-js/src/core/theme/presets/powerbi-light.ts @@ -65,6 +65,20 @@ export const powerbiLight: ThemePreset = { "#ff9d3b", "#4a9c2d" ], + // Continuous measure: Power BI azure, light to deep. + "sequential": { + "stops": [ + "#e5f1ff", + "#b3d7ff", + "#7bbcff", + "#3f9dff", + "#118dff", + "#0a5cb5" + ], + "space": "lab", + "endpointsAgainstSurface": true, + "consumption": "interpolate" + }, "diverging": { "stops": [ "#118dff", diff --git a/packages/flint-js/src/core/theme/presets/powerbi.ts b/packages/flint-js/src/core/theme/presets/powerbi.ts index 72def375..f82c270f 100644 --- a/packages/flint-js/src/core/theme/presets/powerbi.ts +++ b/packages/flint-js/src/core/theme/presets/powerbi.ts @@ -61,6 +61,23 @@ export const powerbi: ThemePreset = { "#ff9d3b", "#4a9c2d" ], + // Continuous measure on a dark canvas: dim blue at the low end + // (never the background) rising to bright azure, so "more" + // reads as brighter — a light-to-dark ramp would sink the high + // values into the near-black plot. + "sequential": { + "stops": [ + "#123049", + "#0f4c86", + "#1170c9", + "#3f9dff", + "#7bbcff", + "#c9e3ff" + ], + "space": "lab", + "endpointsAgainstSurface": true, + "consumption": "interpolate" + }, "diverging": { "stops": [ "#118dff", diff --git a/packages/flint-js/src/vegalite/theme.ts b/packages/flint-js/src/vegalite/theme.ts index fe7a92da..3a57b9a6 100644 --- a/packages/flint-js/src/vegalite/theme.ts +++ b/packages/flint-js/src/vegalite/theme.ts @@ -2619,9 +2619,34 @@ function labelOneBody(spec: any, body: any, d: DesignDecisions, table: any[], sa const declared = normalizeMark(arc?.mark)?.outerRadius; const w = body.width ?? spec.width; const h = body.height ?? spec.height; - const r = typeof declared === 'number' + let r = typeof declared === 'number' ? declared : (typeof w === 'number' && typeof h === 'number' ? Math.min(w, h) / 2 : undefined); + // Vega-Lite grows the wedge to fill the plot box, then hangs an + // outside label at `radius + 14`. With nothing declared the wedge + // already touches the box edge, so a label — centred on the radius + // and reaching out half its own width — runs off the canvas (the + // widest numbers on the 3/9-o'clock slices lose a digit). When the + // house prints its numbers outside, pull the wedge in far enough to + // seat the labels: reserve half the widest label plus the offset, + // and write that radius back onto the arc so the drawn wedge and the + // label ring agree. An arc that states its own radius is left alone. + if (r !== undefined && !inside && typeof declared !== 'number' + && typeof w === 'number' && typeof h === 'number' && arc) { + const labelChars = table.reduce((m, row) => { + const v = row?.[measure.field]; + if (typeof v !== 'number' || !Number.isFinite(v)) return m; + return Math.max(m, Math.round(Math.abs(v)).toLocaleString('en-US').length); + }, 1); + const estHalfWidth = (labelChars * (t.fontSize ?? 10) * 0.62) / 2; + const halfMin = Math.min(w, h) / 2; + const arcOuter = Math.max(halfMin * 0.5, halfMin - (estHalfWidth + 16)); + if (arcOuter < r) { + const norm = normalizeMark(arc.mark) ?? { type: 'arc' }; + arc.mark = { ...norm, outerRadius: Math.round(arcOuter) }; + r = arcOuter; + } + } if (r) { const labelRadius = inside ? r * 0.72 : r + 14; Object.assign(markDef, { radius: labelRadius }); From c6b9a2a5f4b87c4a2ed37fa53a5bdecafc431644 Mon Sep 17 00:00:00 2001 From: Chenglong Wang Date: Fri, 31 Jul 2026 12:43:36 -0700 Subject: [PATCH 054/164] theme(mckinsey): document that the near-black single ink is authentic Deep Blue MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit McKinsey's single-series fill is #051c2c — its 2020 "Deep Blue" brand colour, deliberately almost-black to read as authority against white, and used as the primary data-bar colour in real exhibits and templates. It reads near-black at bar scale by design; electric blue (#2251ff) is the house's highlight, not its default. Added a comment so a future reader doesn't mistake it for an unstyled black fallback and "fix" it to a lighter blue. No behavioural change (single unchanged). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 33e4b998-4b58-4d7e-9cf8-c107b10e45c2 --- packages/flint-js/src/core/theme/presets/mckinsey.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/flint-js/src/core/theme/presets/mckinsey.ts b/packages/flint-js/src/core/theme/presets/mckinsey.ts index 019ef6da..6e9dc0f1 100644 --- a/packages/flint-js/src/core/theme/presets/mckinsey.ts +++ b/packages/flint-js/src/core/theme/presets/mckinsey.ts @@ -33,6 +33,13 @@ export const mckinsey: ThemePreset = { "rule": "#d3dce1" }, "series": { + // McKinsey's 2020 "Deep Blue" (#051c2c) is deliberately + // *almost black* — the firm's own brand refresh describes it + // as near-black to project authority against white, and real + // exhibits/reports use it as the primary data-bar colour. + // So a lone series reads near-black by design; electric blue + // (#2251ff) is the house's highlight, not its default. Kept + // authentic — do not "fix" it to a lighter blue. "single": "#051c2c", "categorical": [ "#051c2c", From adbf8622caf933315b720dcb7e724c09e8ed6c95 Mon Sep 17 00:00:00 2001 From: Chenglong Wang Date: Fri, 31 Jul 2026 12:56:24 -0700 Subject: [PATCH 055/164] =?UTF-8?q?theme(radial):=20seat=20outside=20pie?= =?UTF-8?q?=20labels=20clear=20of=20the=20arc=20(gap=2014=E2=86=9222)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit McKinsey and its editorial peers annotate pie/donut slices with the number sitting clearly off the rim, not stuck to the arc. The outside value-label ring was placed at radius+14, which read as glued to the wedge. Bump the shared radial-outside gap to 22 and reserve the matching gutter when pulling the wedge in, so the drawn arc and the label ring still agree and the widest 3/9-o'clock numbers keep all their digits. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 33e4b998-4b58-4d7e-9cf8-c107b10e45c2 --- packages/flint-js/src/vegalite/theme.ts | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/packages/flint-js/src/vegalite/theme.ts b/packages/flint-js/src/vegalite/theme.ts index 3a57b9a6..a2865ab1 100644 --- a/packages/flint-js/src/vegalite/theme.ts +++ b/packages/flint-js/src/vegalite/theme.ts @@ -2623,14 +2623,18 @@ function labelOneBody(spec: any, body: any, d: DesignDecisions, table: any[], sa ? declared : (typeof w === 'number' && typeof h === 'number' ? Math.min(w, h) / 2 : undefined); // Vega-Lite grows the wedge to fill the plot box, then hangs an - // outside label at `radius + 14`. With nothing declared the wedge + // outside label just past the rim. With nothing declared the wedge // already touches the box edge, so a label — centred on the radius // and reaching out half its own width — runs off the canvas (the // widest numbers on the 3/9-o'clock slices lose a digit). When the // house prints its numbers outside, pull the wedge in far enough to - // seat the labels: reserve half the widest label plus the offset, - // and write that radius back onto the arc so the drawn wedge and the - // label ring agree. An arc that states its own radius is left alone. + // seat the labels with air around them: reserve half the widest + // label plus the gap, and write that radius back onto the arc so the + // drawn wedge and the label ring agree. A snug 14px gap reads as the + // number stuck to the rim; McKinsey and its peers sit the annotation + // clear of the arc, so the gap is generous. An arc that states its + // own radius is left alone. + const radialOutsideGap = 22; if (r !== undefined && !inside && typeof declared !== 'number' && typeof w === 'number' && typeof h === 'number' && arc) { const labelChars = table.reduce((m, row) => { @@ -2640,7 +2644,7 @@ function labelOneBody(spec: any, body: any, d: DesignDecisions, table: any[], sa }, 1); const estHalfWidth = (labelChars * (t.fontSize ?? 10) * 0.62) / 2; const halfMin = Math.min(w, h) / 2; - const arcOuter = Math.max(halfMin * 0.5, halfMin - (estHalfWidth + 16)); + const arcOuter = Math.max(halfMin * 0.5, halfMin - (estHalfWidth + radialOutsideGap + 4)); if (arcOuter < r) { const norm = normalizeMark(arc.mark) ?? { type: 'arc' }; arc.mark = { ...norm, outerRadius: Math.round(arcOuter) }; @@ -2648,7 +2652,7 @@ function labelOneBody(spec: any, body: any, d: DesignDecisions, table: any[], sa } } if (r) { - const labelRadius = inside ? r * 0.72 : r + 14; + const labelRadius = inside ? r * 0.72 : r + radialOutsideGap; Object.assign(markDef, { radius: labelRadius }); // The slice's share of the circle is its value over the total; swung // out to the label radius that share becomes an arc of From 8990ee01aee935e49d5c3f957dcc6e0ba05aeeef Mon Sep 17 00:00:00 2001 From: Chenglong Wang Date: Fri, 31 Jul 2026 13:35:02 -0700 Subject: [PATCH 056/164] theme: fix donut-as-pie, radar legend loss, and waterfall title leak Three bugs surfaced by driving the houses with the real-world r1 dataset set (a new `scripts/theme-real.ts` audit harness), not just the synthetic R2 generators: * Donut Chart rendered as a full pie. Property defaults are not merged into `chartProperties` at assemble time, so a donut authored without an explicit `innerRadius` inherited the pie's hole-less 0. Give the Donut template a non-zero default and apply it before delegating to the pie instantiate. (mobile-donut, and the test-data real donut, were pies.) * Radar series were unidentifiable on every house but datawrapper. The template put `legend: null` on the fill and point-colour encodings; in a layered spec the colour scale is shared, so a null on any one layer strips the key from the stroke too. Let stroke/fill/colour share the one merged legend. And a radar polygon is a closed run with no "last point", so `demoteSeriesEnd` now falls back to a drawn legend for `interpolate: *-closed` marks instead of hanging end labels on an arbitrary spoke (nyt/mckinsey previously showed neither legend nor label). * Nature waterfall y-axis title read "Population (M) __wf_connector_y (M)". `titleOf` fell through to the raw field name when a shared-scale helper layer set `title: null`; it now respects an explicit null and never surfaces internal `__`-prefixed field names as an axis title. Gates: flint tsc, 694 vitest, r2 85/0, theme-audit 62/0. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 33e4b998-4b58-4d7e-9cf8-c107b10e45c2 --- .../flint-js/src/vegalite/templates/pie.ts | 19 +++++++++++++++ .../flint-js/src/vegalite/templates/radar.ts | 9 +++++-- .../src/vegalite/templates/waterfall.ts | 6 ++--- packages/flint-js/src/vegalite/theme.ts | 24 +++++++++++++++---- 4 files changed, 49 insertions(+), 9 deletions(-) diff --git a/packages/flint-js/src/vegalite/templates/pie.ts b/packages/flint-js/src/vegalite/templates/pie.ts index d8eb4c3b..d611efe7 100644 --- a/packages/flint-js/src/vegalite/templates/pie.ts +++ b/packages/flint-js/src/vegalite/templates/pie.ts @@ -108,7 +108,26 @@ export const pieChartDef: ChartTemplateDef = { ] as ChartPropertyDef[], }; +/** The hole a Donut Chart gets when the caller sets no `innerRadius`. */ +const DONUT_DEFAULT_INNER_RADIUS = 50; + export const donutChartDef: ChartTemplateDef = { ...pieChartDef, chart: "Donut Chart", + // A donut is a pie with a hole. Property defaults are not merged into + // `chartProperties` at assemble time (only discrete options are coerced), + // so a Donut Chart authored without an explicit `innerRadius` would inherit + // the pie's hole-less 0 and render as a full pie. Carry a non-zero default + // and apply it here before delegating to the pie's instantiate. + properties: (pieChartDef.properties ?? []).map((p) => + p.key === 'innerRadius' ? { ...p, defaultValue: DONUT_DEFAULT_INNER_RADIUS } : p, + ) as ChartPropertyDef[], + instantiate: (spec, ctx) => { + const innerRadius = ctx.chartProperties?.innerRadius; + const withHole = + innerRadius == null + ? { ...ctx, chartProperties: { ...(ctx.chartProperties ?? {}), innerRadius: DONUT_DEFAULT_INNER_RADIUS } } + : ctx; + pieChartDef.instantiate(spec, withHole); + }, }; diff --git a/packages/flint-js/src/vegalite/templates/radar.ts b/packages/flint-js/src/vegalite/templates/radar.ts index ef491dc1..31682cbf 100644 --- a/packages/flint-js/src/vegalite/templates/radar.ts +++ b/packages/flint-js/src/vegalite/templates/radar.ts @@ -230,9 +230,14 @@ function buildRadarLayers( }, }; if (groups.length > 1 && groupField) { + // Stroke, fill and point colour all carry `__group`; Vega-Lite merges + // them into a single legend for the field. Leaving the legend off any + // one of them (a `legend: null`) removes the key from *all* of them, + // because the layers share the colour scale — so the series would be + // unidentifiable. Let all three share the one merged legend. lineLayer.encoding.stroke = { field: "__group", type: "nominal", title: groupField }; if (filled) { - lineLayer.encoding.fill = { field: "__group", type: "nominal", title: groupField, legend: null }; + lineLayer.encoding.fill = { field: "__group", type: "nominal", title: groupField }; } } else if (filled) { lineLayer.mark.fill = "#4c78a8"; @@ -254,7 +259,7 @@ function buildRadarLayers( }, }; if (groups.length > 1 && groupField) { - pointLayer.encoding.color = { field: "__group", type: "nominal", title: groupField, legend: null }; + pointLayer.encoding.color = { field: "__group", type: "nominal", title: groupField }; } layers.push(pointLayer); diff --git a/packages/flint-js/src/vegalite/templates/waterfall.ts b/packages/flint-js/src/vegalite/templates/waterfall.ts index f2f1a321..ac73a85c 100644 --- a/packages/flint-js/src/vegalite/templates/waterfall.ts +++ b/packages/flint-js/src/vegalite/templates/waterfall.ts @@ -211,7 +211,7 @@ export const waterfallChartDef: ChartTemplateDef = { encoding: { x: { field: xField, type: "ordinal", sort: null, bandPosition: 0 }, x2: { field: "__wf_lead", bandPosition: 1 }, - y: { field: "__wf_connector_y", type: "quantitative" }, + y: { field: "__wf_connector_y", type: "quantitative", title: null }, }, }, ]; @@ -239,7 +239,7 @@ export const waterfallChartDef: ChartTemplateDef = { fill: "#374151", }, encoding: { - y: { field: "__wf_sum", type: "quantitative" }, + y: { field: "__wf_sum", type: "quantitative", title: null }, text: { field: "__wf_sum", type: "quantitative", format: labelFormat }, }, }, @@ -254,7 +254,7 @@ export const waterfallChartDef: ChartTemplateDef = { fontSize: labelFontSize, }, encoding: { - y: { field: "__wf_center", type: "quantitative" }, + y: { field: "__wf_center", type: "quantitative", title: null }, text: { field: "__wf_delta_text", type: "nominal" }, color: { condition: { test: "datum.__wf_color === 'total'", value: "#725a30" }, diff --git a/packages/flint-js/src/vegalite/theme.ts b/packages/flint-js/src/vegalite/theme.ts index a2865ab1..7f08a01f 100644 --- a/packages/flint-js/src/vegalite/theme.ts +++ b/packages/flint-js/src/vegalite/theme.ts @@ -215,8 +215,15 @@ function mergedEncoding(node: any, inherited: any): any { /** The words Vega-Lite will write on this axis if nobody says otherwise. */ function titleOf(enc: any): string | undefined { + // An explicit `title: null` is the author saying "this layer carries no + // title" — a shared-scale helper layer (a waterfall connector, a label + // series) that must not contribute its field name to the merged axis title. + if (enc?.title === null) return undefined; if (typeof enc?.title === 'string') return enc.title; - return typeof enc?.field === 'string' ? enc.field : undefined; + // Internal helper fields (`__wf_connector_y`, …) are plumbing, never a + // reader-facing title. + if (typeof enc?.field === 'string' && !enc.field.startsWith('__')) return enc.field; + return undefined; } // --------------------------------------------------------------------------- @@ -2964,14 +2971,23 @@ function demoteSeriesEnd(spec: any, d: DesignDecisions, say: (p: string, m: stri const runsAlongX = runChannel(d) === 'x'; const marginTaken = bands && (runsAlongX ? d.axes.y?.orient === 'right' : d.axes.x?.orient === 'top'); + // A closed run (a radar/spider polygon, `interpolate: *-closed`) has no last + // point: every vertex is an axis, not an end, so an end label would land on + // an arbitrary spoke. The series can only be named by a key. + const closedRun = units.some((n: any) => { + const interp = normalizeMark(n.mark)?.interpolate; + return typeof interp === 'string' && interp.endsWith('-closed'); + }); const reason = units.length === 0 ? '`seriesEnd` needs a line mark' : (!field ? '`seriesEnd` needs a series field to name' : (field === d.bound.categoryField ? '`seriesEnd` would restate the categorical axis' - : marginTaken - ? `the ${runsAlongX ? 'right' : 'top'} margin holds the value axis, so a name too big for its band has nowhere to stand` - : null)); + : closedRun + ? '`seriesEnd` needs an open run — a closed polygon has no last point' + : marginTaken + ? `the ${runsAlongX ? 'right' : 'top'} margin holds the value axis, so a name too big for its band has nowhere to stand` + : null)); if (!reason) return; // The house ranked its placements; a demotion should land on the next one // it named, not on whatever this function happens to prefer. From 88418035c1ae481a11219515f27821f2e735d94d Mon Sep 17 00:00:00 2001 From: Chenglong Wang Date: Fri, 31 Jul 2026 14:00:08 -0700 Subject: [PATCH 057/164] theme: keep candlestick price axis, show pie share %, stop legend truncation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Batch-2 real-data audit fixes (real datasets rendered through the PNG harness): - Candlestick loses its price axis on nyt & mckinsey. Both houses print per-mark values instead of a measure axis, but a candlestick is a multi-value glyph (open/high/low/close) — no single scalar to print — so the axis was dropped with nothing replacing it and prices became unreadable. New MULTI_VALUE_GLYPH_CHARTS set makes such charts non-labelable (like the isSummarised escape), so dlShow stays false and the measure axis suppression never fires. Prices readable on all 8 houses. - Pie slices lose the % on unit-never houses (nature, mckinsey). A part-to- whole value whose slices sum to 100 *is* a percentage; the % is the number's meaning, not an axis-style flourish, and a pie has no ruler to carry it. It now rides on the printed value whatever the house's axis-unit policy. Gated by percentOfWhole (only fires on values totalling 100), so a pie of raw amounts keeps its bare numbers. - Legend collapses to "...2 entries" on a 5-series stacked bar (nyt, economist, datawrapper), hiding two bands the colour key is the only means to identify. A top/bottom legend spans the whole chart, not the narrow plot the bars occupy, so a ~90px stacked column wrongly forced columns=1 and a columns*4 = 4 cap. Floor the block width for horizontal legends and never cap a nominal key below a full palette's worth — the "Others (N)" fold handles genuine high cardinality upstream. Gates: flint+site tsc, 695 vitest, r2 0/85, theme-audit 0/62. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 33e4b998-4b58-4d7e-9cf8-c107b10e45c2 --- packages/flint-js/src/core/theme/ground.ts | 34 +++++++++++++++---- packages/flint-js/src/vegalite/theme.ts | 24 +++++++++++-- packages/flint-js/tests/theme-presets.test.ts | 27 +++++++++++++-- 3 files changed, 74 insertions(+), 11 deletions(-) diff --git a/packages/flint-js/src/core/theme/ground.ts b/packages/flint-js/src/core/theme/ground.ts index 9d638148..568ba89f 100644 --- a/packages/flint-js/src/core/theme/ground.ts +++ b/packages/flint-js/src/core/theme/ground.ts @@ -309,6 +309,14 @@ const DISTRIBUTION_SHAPE_CHARTS = new Set(['Violin Plot', 'Density Plot']); // the bar is a secondary in-cell glyph and the category axis is a row-header // gutter, not a base the bars stand on. const TABLE_CHARTS = new Set(['Bar Table']); +// A multi-value glyph carries several measures in one mark — a candlestick is +// open/high/low/close — so there is no single scalar per datum to print. The +// mark itself is the value; a lone number stamped on it would name one of four +// prices and mislead on the other three. These charts look labelable (a measure +// on a position, a banded axis, not a distribution summary) but must never +// print a value, and — the reason this matters — must never let a house drop +// the measure axis on the false premise that the value is printed elsewhere. +const MULTI_VALUE_GLYPH_CHARTS = new Set(['Candlestick Chart']); // The title block's vertical rhythm, as a multiple of the headline / deck font // size. A house's whitespace personality reaches the title here: `tight` packs @@ -912,16 +920,19 @@ export function groundTheme(themeIn: ThemeSpec, ctx: GroundingContext): DesignDe const labelable = ((signals.hasBandedAxis && (bindings.measureChannels.length > 0 || gridCells)) || signals.isPartToWhole) && !ctx.positional?.stacked - && !signals.isSummarised; + && !signals.isSummarised + && !MULTI_VALUE_GLYPH_CHARTS.has(ctx.chartType); if (dlShow && !labelable) { dlShow = false; say('dataLabels.show', ctx.positional?.stacked ? 'the segments are stacked — a value at a segment edge would read as the running total' : signals.isSummarised ? 'the chart summarises a distribution — each band holds a sample, not one quantity to print' - : signals.hasBandedAxis - ? 'the measure is not on an axis — there is no position to print a value at' - : 'no banded axis to key values to — one number per datum would be noise, not a label'); + : MULTI_VALUE_GLYPH_CHARTS.has(ctx.chartType) + ? 'the mark carries several measures at once — there is no single value to print, and the measure axis stays as the only reading of them' + : signals.hasBandedAxis + ? 'the measure is not on an axis — there is no position to print a value at' + : 'no banded axis to key values to — one number per datum would be noise, not a label'); } if (dl.show === 'always' && dlShow) { @@ -1038,10 +1049,19 @@ export function groundTheme(themeIn: ThemeSpec, ctx: GroundingContext): DesignDe ?? (['theta', 'size', 'radius'] as const).find((ch) => ctx.channelSemantics?.[ch]?.field); const axisStatesUnit = (['x', 'y'] as const) .some((ch) => axes[ch]?.unit && axes[ch]!.label.show !== false); - const valueUnit = (theme.annotation?.unit ?? 'never') !== 'never' && !axisStatesUnit - ? (unitText(ctx, valueUnitChannel ?? '') - ?? (signals.isPartToWhole ? percentOfWhole(ctx, valueUnitChannel ?? '') : undefined)) + const houseStatesUnit = (theme.annotation?.unit ?? 'never') !== 'never' && !axisStatesUnit; + // A part-to-whole value whose slices sum to 100 *is* a percentage — the `%` + // is the number's meaning, not a house-style flourish, and a pie has no + // ruler to carry it. So it rides on the printed value whatever the house's + // axis-unit policy: a bare `65` on a slice reads as a count, not a share. + // (`percentOfWhole` only fires on values that actually total 100, so a pie + // of raw amounts keeps its bare numbers.) + const shareUnit = signals.isPartToWhole && !axisStatesUnit + ? percentOfWhole(ctx, valueUnitChannel ?? '') : undefined; + const valueUnit = houseStatesUnit + ? (unitText(ctx, valueUnitChannel ?? '') ?? shareUnit) + : shareUnit; // A label placed at the mark sits *inside* it, which only works while the // mark is longer than the label. Below that length the label has to move diff --git a/packages/flint-js/src/vegalite/theme.ts b/packages/flint-js/src/vegalite/theme.ts index 7f08a01f..564940ee 100644 --- a/packages/flint-js/src/vegalite/theme.ts +++ b/packages/flint-js/src/vegalite/theme.ts @@ -2250,6 +2250,13 @@ function wrapWideKeys( ): void { if (!block) return; const MAX_ROWS = 4; + // A top/bottom legend has the whole chart width to flow across; the base + // display is 300px wide, so a narrow plot's key still has at least this + // much room before it has to stack into one column. + const MIN_HORIZONTAL_LEGEND_BLOCK = 280; + // A full categorical palette's worth of names always fits — the point past + // which high cardinality has already been folded into "Others (N)". + const MIN_LEGEND_ENTRIES = 12; walk(spec, (node) => { for (const channel of ['color', 'fill', 'stroke'] as const) { const enc = node.encoding?.[channel]; @@ -2265,7 +2272,13 @@ function wrapWideKeys( const symbol = symbolArea ? 2 * Math.sqrt(symbolArea / Math.PI) : 10; const entryWidth = entries.reduce( (m, e) => Math.max(m, symbol + 4 + e.length * labelFS * 0.55 + 10), 0); - const columns = Math.max(1, Math.floor(block / entryWidth)); + // A top or bottom legend flows across the whole chart, not just the + // plot the marks occupy. A five-bar stacked column is ~90px wide + // yet its key has the full canvas to spread over, so a narrow plot + // must not force the key into one column (and, below, into a cap + // that would then hide entries). + const usableBlock = Math.max(block, MIN_HORIZONTAL_LEGEND_BLOCK); + const columns = Math.max(1, Math.floor(usableBlock / entryWidth)); if (entries.length <= columns) continue; enc.legend = { ...(enc.legend ?? {}), columns }; // A folded key is already the bounded top-K + "Others (N)" list the @@ -2273,7 +2286,14 @@ function wrapWideKeys( // the very Others row that stands in for the tail. Wrap it into // columns, but never truncate it. const folded = enc.field === OVERFLOW_KEY_FIELD; - const cap = columns * MAX_ROWS; + // The cap bounds a genuine *tower* of names — dozens of entries no + // grid can hold. It must never hide the handful of series a normal + // key names: on a stacked bar or an area the colour key is the only + // thing telling those series apart, so truncating it to "…2 entries" + // leaves two bands unidentifiable. Never cap below a full + // categorical palette's worth (past which the "Others (N)" fold + // would already have engaged upstream). + const cap = Math.max(columns * MAX_ROWS, MIN_LEGEND_ENTRIES); if (!folded && entries.length > cap) enc.legend.symbolLimit = cap; say('legend.columns', `${entries.length} keys in one row overrun the ${Math.round(block)}px block — wrapped to ${columns} columns`); diff --git a/packages/flint-js/tests/theme-presets.test.ts b/packages/flint-js/tests/theme-presets.test.ts index 640aca99..1f8bf8bc 100644 --- a/packages/flint-js/tests/theme-presets.test.ts +++ b/packages/flint-js/tests/theme-presets.test.ts @@ -222,8 +222,31 @@ describe('a printed value carries the unit no axis can', () => { expect(JSON.stringify(transform)).toContain('%'); }); - it('leaves the value bare where the house asks for no unit', () => { - const { text } = labelText(pie({})); + it('carries the share % even where the house states no unit', () => { + // A pie slice summing to 100 *is* a percentage — the % is the number's + // meaning, not a house flourish, and the pie has no axis to hold it. So + // it rides on the value whatever the house's axis-unit policy. + const { text, transform } = labelText(pie({})); + expect(text?.field).toBe('__flintValueWithUnit'); + expect(JSON.stringify(transform)).toContain('%'); + }); + + it('leaves raw amounts bare — a pie that is not shares keeps its numbers', () => { + const AMOUNTS = [ + { Browser: 'Chrome', Share: 650 }, + { Browser: 'Safari', Share: 200 }, + { Browser: 'Edge', Share: 100 }, + { Browser: 'Other', Share: 50 }, + ]; + const spec = assembleVegaLite({ + data: { values: AMOUNTS }, + semantic_types: { Browser: 'Category', Share: 'Quantity' }, + chart_spec: { chartType: 'Pie Chart', encodings: { size: 'Share', color: 'Browser' } }, + theme_spec: theme({ + dataLabels: { show: 'always', placement: 'atMark', inkMode: 'fixed' }, + }), + } as any) as any; + const { text } = labelText(spec); expect(text?.field).toBe('Share'); }); }); From dfe91a0b70fc4fe4b88775006f1b17d838c50756 Mon Sep 17 00:00:00 2001 From: Chenglong Wang Date: Fri, 31 Jul 2026 14:07:24 -0700 Subject: [PATCH 058/164] =?UTF-8?q?playground:=20add=20"Theme=20lab=20real?= =?UTF-8?q?"=20page=20=E2=80=94=20real=20datasets=20=C3=97=20all=20houses?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Browser twin of the audit-out/real/ contact sheets. Renders the r1 theme-lab real-world datasets (new-case-preview-data.ts, VL-supported only) as Flint plus all seven houses, so the real-corpus outputs can be inspected alongside the synthetic R2 grid. Same lazy-compile + fixed-tile discipline as ThemeLabR2Cell (300x300 base, IntersectionObserver compile-on-scroll); the corpus is chunked into fixed-size pages so the DOM never holds more than a handful of rows. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 33e4b998-4b58-4d7e-9cf8-c107b10e45c2 --- site/src/main.tsx | 2 + site/src/playground/PlaygroundShell.tsx | 1 + site/src/playground/ThemeLabReal.tsx | 137 ++++++++++++++++++ site/src/playground/ThemeLabRealCell.tsx | 173 +++++++++++++++++++++++ 4 files changed, 313 insertions(+) create mode 100644 site/src/playground/ThemeLabReal.tsx create mode 100644 site/src/playground/ThemeLabRealCell.tsx diff --git a/site/src/main.tsx b/site/src/main.tsx index c74baa4a..ebc9c96e 100644 --- a/site/src/main.tsx +++ b/site/src/main.tsx @@ -17,6 +17,7 @@ import { Labs } from './playground/Labs'; import { DemoWall } from './playground/DemoWall'; import { ThemeLab } from './playground/ThemeLab'; import { ThemeLabR2 } from './playground/ThemeLabR2'; +import { ThemeLabReal } from './playground/ThemeLabReal'; import { ThemeLabGaps } from './playground/ThemeLabGaps'; import { FullTestCases } from './playground/FullTestCases'; import { LocaleProvider, useLocale } from './i18n/LocaleContext'; @@ -57,6 +58,7 @@ function AppRoutes({ locale }: { locale: Locale }) { } /> } /> } /> + } /> } /> } /> diff --git a/site/src/playground/PlaygroundShell.tsx b/site/src/playground/PlaygroundShell.tsx index 77b33e05..72702e00 100644 --- a/site/src/playground/PlaygroundShell.tsx +++ b/site/src/playground/PlaygroundShell.tsx @@ -9,6 +9,7 @@ const pages = [ { to: 'demo-wall', label: 'Demo wall' }, { to: 'theme-labs', label: 'Theme lab' }, { to: 'theme-lab-r2', label: 'Theme lab R2' }, + { to: 'theme-lab-real', label: 'Theme lab real' }, { to: 'theme-lab-gaps', label: 'Theme lab gaps' }, { to: 'full-test-cases', label: 'Full test cases' }, ]; diff --git a/site/src/playground/ThemeLabReal.tsx b/site/src/playground/ThemeLabReal.tsx new file mode 100644 index 00000000..54c451a8 --- /dev/null +++ b/site/src/playground/ThemeLabReal.tsx @@ -0,0 +1,137 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** + * Theme lab — real data. + * + * The R2 page drives the houses with synthetic gallery generators (clean + * cardinalities, tidy labels). This page drives them with the *real-world* + * datasets in `new-case-preview-data.ts` (the r1 theme-lab set): real category + * names, real distributions, negatives and long labels. It is the browser twin + * of the `audit-out/real/` contact sheets (`scripts/theme-real.ts`). + * + * One row per case, eight columns: Flint's default plus every house. Only the + * VL-supported cases are shown (Plotly-only cases have no ThemeSpec path). The + * corpus is chunked into fixed-size pages so the DOM never holds more than a + * handful of rows, and each cell compiles only when it scrolls into view. + */ + +import { useState, type ReactNode } from 'react'; +import { vlGetTemplateDef } from 'flint-chart'; +import { siteTheme } from '../shared/theme'; +import { PREVIEW_CASES, type PreviewCase } from './new-case-preview-data'; +import { RealCell, REAL_COLUMNS } from './ThemeLabRealCell'; + +/** VL-supported real cases (Plotly-only cases have no ThemeSpec path). */ +const REAL_CASES: PreviewCase[] = PREVIEW_CASES.filter((c) => vlGetTemplateDef(c.chartType)); + +const PAGE_SIZE = 6; +const PAGE_COUNT = Math.ceil(REAL_CASES.length / PAGE_SIZE); + +function Pill({ children }: { children: ReactNode }) { + return ( + + {children} + + ); +} + +function Row({ c }: { c: PreviewCase }) { + return ( +
+
+
+ {c.id} · {c.title} +
+ {c.blurb ? ( +
{c.blurb}
+ ) : null} +
+ {c.chartType} + {c.source ? src: {c.source} : null} +
+
+
+ {REAL_COLUMNS.map((col) => ( + + ))} +
+
+ ); +} + +export function ThemeLabReal() { + const [page, setPage] = useState(0); + const start = page * PAGE_SIZE; + const cases = REAL_CASES.slice(start, start + PAGE_SIZE); + + return ( +
+
+

+ Theme lab · real data +

+

+ The r1 theme-lab real-world datasets, each read as Flint plus all seven houses — the + browser twin of the audit-out/real/ contact sheets. Real labels, + distributions and negatives stress the themes in ways the synthetic R2 corpus does not. + {' '}{REAL_CASES.length} VL-supported cases. +

+
+ + + + {cases.map((c) => ( + + ))} +
+ ); +} diff --git a/site/src/playground/ThemeLabRealCell.tsx b/site/src/playground/ThemeLabRealCell.tsx new file mode 100644 index 00000000..9b3d9a09 --- /dev/null +++ b/site/src/playground/ThemeLabRealCell.tsx @@ -0,0 +1,173 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** + * A single (real-dataset case × column) cell of the real-data theme grid. Same + * lazy-compile discipline as `ThemeLabR2Cell`: it assembles its spec — Flint's + * default, or one house's ThemeSpec on top — only the first time it scrolls + * into view, so a page of real-world cases across eight columns pays only for + * the panels a reader is actually looking at. + * + * The input mirrors `scripts/theme-real.ts` `inputFor` (and `ThemeLab.tsx`), so + * a cell here shows exactly what the `audit-out/real/` contact sheets show. + */ + +import { useEffect, useMemo, useRef, useState } from 'react'; +import { THEME_PRESETS, assembleVegaLite } from 'flint-chart'; +import { VegaLiteView } from '../components/VegaLiteView'; +import { ScaleToFit } from '../components/ScaleToFit'; +import { siteTheme } from '../shared/theme'; +import type { PreviewCase } from './new-case-preview-data'; + +export const REAL_COLUMNS = ['flint', ...Object.keys(THEME_PRESETS)] as const; +export type RealColumn = (typeof REAL_COLUMNS)[number]; + +/** Tile geometry — fixed so a chart is never flex-shrunk below legibility. */ +export const REAL_TILE_WIDTH = 320; +const REAL_TILE_HEIGHT = 320; + +/** Assembly input for a real case — mirrors `scripts/theme-real.ts` `inputFor`. */ +export function realInput(c: PreviewCase): any { + return { + data: { values: c.data }, + semantic_types: c.semantic_types, + chart_spec: { + chartType: c.chartType, + title: c.title, + encodings: c.encodings, + baseSize: { width: 300, height: 300 }, + ...(c.chartProperties ? { chartProperties: c.chartProperties } : {}), + }, + }; +} + +function stripInternal(node: any): void { + if (!node || typeof node !== 'object') return; + if (Array.isArray(node)) return node.forEach(stripInternal); + for (const key of Object.keys(node)) { + if (/^_[^_]/.test(key)) delete node[key]; + else stripInternal(node[key]); + } +} + +interface Compiled { + spec?: any; + background: string; + error?: string; + reportCount: number; +} + +function compileCell(c: PreviewCase, column: RealColumn): Compiled { + try { + const input = realInput(c); + const themeId = column === 'flint' ? null : column; + const spec = assembleVegaLite( + themeId ? { ...input, theme_spec: (THEME_PRESETS as any)[themeId].spec } : input, + ) as any; + const reportCount = spec._theme?.report?.length ?? 0; + const background = typeof spec.background === 'string' ? spec.background : '#ffffff'; + stripInternal(spec); + delete spec.$schema; + return { spec, background, reportCount }; + } catch (err) { + return { background: '#ffe8e8', error: (err as Error).message, reportCount: 0 }; + } +} + +export function RealCell({ c, column }: { c: PreviewCase; column: RealColumn }) { + const ref = useRef(null); + const [visible, setVisible] = useState(false); + + useEffect(() => { + const el = ref.current; + if (!el) return; + const observer = new IntersectionObserver( + ([entry]) => { + if (entry.isIntersecting) { + setVisible(true); + observer.disconnect(); + } + }, + { rootMargin: '300px' }, + ); + observer.observe(el); + return () => observer.disconnect(); + }, []); + + const built = useMemo(() => (visible ? compileCell(c, column) : null), [visible, c, column]); + + return ( +
+
+ {column} + {built && !built.error ? ( + {built.reportCount} notes + ) : null} +
+
+ {!built ? ( +
+ … +
+ ) : built.error ? ( +
+ {built.error} +
+ ) : ( + + + + )} +
+
+ ); +} From b046c8d9678897105a66fa3858f283aee98dd671 Mon Sep 17 00:00:00 2001 From: Chenglong Wang Date: Fri, 31 Jul 2026 14:19:22 -0700 Subject: [PATCH 059/164] theme: bin economist diverging heatmap into stepped quantize scale Economist's diverging colour ramp interpolated continuously over muted warm tones, so a one-sided temperature field washed out into barely distinguishable tan/brown cells. Real Economist choropleths use binned steps for clear distinctions. Give the diverging ramp consumption: quantize (5 bins) like datawrapper. Also fix the legend: a quantize colour scale renders one swatch per bin, but the maxSwatches value-sampling pass (economist declares maxSwatches) was collapsing that stepped key into a couple of round end-labels over a gradient bar. Skip value sampling for discretizing colour scales (quantize/quantile/threshold/bin-ordinal); sequential/continuous ramps still get sampled round labels. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 33e4b998-4b58-4d7e-9cf8-c107b10e45c2 --- packages/flint-js/src/core/theme/presets/economist.ts | 3 ++- packages/flint-js/src/vegalite/theme.ts | 5 +++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/flint-js/src/core/theme/presets/economist.ts b/packages/flint-js/src/core/theme/presets/economist.ts index 6300199b..d6af8392 100644 --- a/packages/flint-js/src/core/theme/presets/economist.ts +++ b/packages/flint-js/src/core/theme/presets/economist.ts @@ -69,7 +69,8 @@ export const economist: ThemePreset = { "neutral": "#e9e5dc", "space": "lab", "endpointsAgainstSurface": true, - "consumption": "interpolate" + "consumption": "quantize", + "quantizeCount": 5 }, "status": { "positive": "#006ba2", diff --git a/packages/flint-js/src/vegalite/theme.ts b/packages/flint-js/src/vegalite/theme.ts index 564940ee..4eac56e3 100644 --- a/packages/flint-js/src/vegalite/theme.ts +++ b/packages/flint-js/src/vegalite/theme.ts @@ -2185,6 +2185,11 @@ function applyLegend(spec: any, config: any, d: DesignDecisions, table: any[], s const enc = node.encoding?.[channel]; if (!enc?.field || enc.type !== 'quantitative' || enc.legend === null) continue; if (channel === 'color' && !enc.scale?.type) continue; + // Discretizing colour scales (quantize/quantile/threshold) already + // render one swatch per bin — sampling round tick values collapses + // that stepped key into a couple of end-labels over a gradient bar. + if (channel === 'color' + && ['quantize', 'quantile', 'threshold', 'bin-ordinal'].includes(enc.scale?.type)) continue; if (enc.legend?.values) continue; const values = roundSample(table, enc.field, l.maxSwatches!); if (!values) continue; From bb2e4df8909f0a1419ca3b6af72dcf17d0bb1e0c Mon Sep 17 00:00:00 2001 From: Chenglong Wang Date: Fri, 31 Jul 2026 14:31:50 -0700 Subject: [PATCH 060/164] theme: keep measure axis left when series names sit at the line ends A house that seats its measure axis on the right (economist's opposite placement, iter 19) collided with the series names a bump/line chart prints at each line's right-hand end: the rank-axis ticks stacked under the labels as 'Uni1ted', 'Ch2na', 'Ja3an'. When the legend resolves to seriesEnd/inline placement, the right margin is already claimed by the names, so any measure axis oriented right falls back to the left (where nyt and mckinsey already put it). Generalizes to economist line-forecast and any opposite-axis house with right-end series labels. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 33e4b998-4b58-4d7e-9cf8-c107b10e45c2 --- packages/flint-js/src/core/theme/ground.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/packages/flint-js/src/core/theme/ground.ts b/packages/flint-js/src/core/theme/ground.ts index 568ba89f..9eb469a4 100644 --- a/packages/flint-js/src/core/theme/ground.ts +++ b/packages/flint-js/src/core/theme/ground.ts @@ -871,6 +871,23 @@ export function groundTheme(themeIn: ThemeSpec, ctx: GroundingContext): DesignDe ? 'none' : (placement as 'top' | 'right' | 'bottom' | 'left'); + // A chart that prints each series' name at its right-hand end (seriesEnd / + // inline placement on a left-to-right axis) has claimed the right margin. + // A house that also seats its measure axis on the right (opposite + // placement) would stack that axis's tick labels under the names — + // "Uni1ted", "Ch2na", "Ja3an" on a bump chart's rank axis. The end labels + // own the right side; the measure axis falls back to the left. + if (placement === 'seriesEnd' || placement === 'inline') { + for (const ch of bindings.measureChannels) { + const ax = axes[ch]; + if (ax && ax.orient === 'right') { + ax.orient = 'left'; + say('axes.measure.placement', + 'the series names sit at the line ends on the right — the measure axis moves to the left so its ticks do not land under the names'); + } + } + } + // A key to a set of names needs no title: `Chrome`, `Safari`, `Firefox` // say what kind of thing they are, and `Browser` written over them repeats // it. A ramp of numbers says nothing of the sort — `26` is an instance of From ee52b4d5478a4ebcf1a3a74ca51a9b73812851f5 Mon Sep 17 00:00:00 2001 From: Chenglong Wang Date: Fri, 31 Jul 2026 14:45:09 -0700 Subject: [PATCH 061/164] theme: keep the size value-key legend when a chart names no series MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A bubble map encodes its measure only in circle size — it has no colour series, so grounding's legend.show came back false. applyLegend then swept away every channel's legend, size included, leaving the bubbles with no key to decode. But 'show a legend' is a decision about naming series; a quantitative size/opacity encoding is a value key and lives by a different rule. Preserve it (and style/seat it in the house's legend type) even when there is no series legend, and likewise under seriesEnd/inline placement where the series names ride on the marks. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 33e4b998-4b58-4d7e-9cf8-c107b10e45c2 --- packages/flint-js/src/vegalite/theme.ts | 30 +++++++++++- packages/flint-js/tests/theme-presets.test.ts | 47 +++++++++++++++++++ 2 files changed, 75 insertions(+), 2 deletions(-) diff --git a/packages/flint-js/src/vegalite/theme.ts b/packages/flint-js/src/vegalite/theme.ts index 4eac56e3..4cbacded 100644 --- a/packages/flint-js/src/vegalite/theme.ts +++ b/packages/flint-js/src/vegalite/theme.ts @@ -2075,13 +2075,39 @@ function applyLegend(spec: any, config: any, d: DesignDecisions, table: any[], s const l = d.legend; if (!l.show || l.placement === 'seriesEnd' || l.placement === 'inline') { + // The legend "show" decision (and seriesEnd/inline placement) is about + // *naming series* — the colour/shape key. A quantitative size or + // opacity encoding is a different kind of legend: a value key, like a + // bubble map's size scale. It has to survive even when there is no + // series legend, or the reader cannot decode how big is how much. + let keptValueKey = false; walk(spec, (node) => { for (const channel of ['color', 'fill', 'stroke', 'shape', 'size', 'opacity'] as const) { const enc = node.encoding?.[channel]; - if (enc?.field) enc.legend = null; + if (!enc?.field) continue; + if ((channel === 'size' || channel === 'opacity') && enc.type === 'quantitative') { + keptValueKey = true; + continue; + } + enc.legend = null; } }); - if (!l.show) return; + if (!keptValueKey) return; + // A value key survives; the series-naming placement ('none' for + // seriesEnd, or no legend at all) does not govern it. Style it in the + // house's legend type and seat it to the side. + config.legend = { + ...(config.legend ?? {}), + orient: !l.orient || l.orient === 'none' ? 'right' : l.orient, + labelFont: l.label.font, + labelFontSize: l.label.fontSize, + labelColor: l.label.color, + titleFont: l.label.font, + titleFontSize: l.label.fontSize, + titleColor: d.text.muted, + }; + say('legend.valueKey', + 'no series legend, but the size key is a value scale — it stays so the bubble sizes can be read'); return; } diff --git a/packages/flint-js/tests/theme-presets.test.ts b/packages/flint-js/tests/theme-presets.test.ts index 1f8bf8bc..ceed1751 100644 --- a/packages/flint-js/tests/theme-presets.test.ts +++ b/packages/flint-js/tests/theme-presets.test.ts @@ -620,3 +620,50 @@ describe('holding cells apart', () => { expect(cell(spec).strokeWidth).toBe(1.5); }); }); + +/** + * A size or opacity encoding is a *value* key, not a series-name key. A bubble + * map has no colour series — its whole legend is the size scale — so the + * "no series legend" decision must not sweep the size key away with it. + */ +describe('keeping a value key when there is no series legend', () => { + const CITIES = [ + { City: 'Tokyo', lon: 139, lat: 35, Pop: 37 }, + { City: 'Delhi', lon: 77, lat: 28, Pop: 32 }, + { City: 'Cairo', lon: 31, lat: 30, Pop: 22 }, + ]; + const bubble = (themeSpec: ThemeSpec) => assembleVegaLite({ + data: { values: CITIES }, + semantic_types: { City: 'Category', lon: 'Longitude', lat: 'Latitude', Pop: 'Quantity' }, + chart_spec: { + chartType: 'Scatter Plot', + encodings: { x: 'lon', y: 'lat', size: 'Pop' }, + }, + theme_spec: themeSpec, + } as any) as any; + + function sizeEnc(spec: any): any { + let found: any; + walkSpec(spec, (n) => { + const e = n.encoding?.size; + if (e?.field === 'Pop') found = e; + }); + return found; + } + function walkSpec(node: any, visit: (n: any) => void): void { + if (!node || typeof node !== 'object') return; + visit(node); + for (const key of ['layer', 'concat', 'hconcat', 'vconcat', 'spec']) { + const child = (node as any)[key]; + if (Array.isArray(child)) child.forEach((c) => walkSpec(c, visit)); + else if (child) walkSpec(child, visit); + } + } + + it('does not null the size legend when the chart names no series', () => { + const spec = bubble(theme({ ink: { surface: { canvas: '#ffffff' } } } as Partial)); + const size = sizeEnc(spec); + expect(size).toBeTruthy(); + expect(size.legend).not.toBeNull(); + }); +}); From 86424e211e41de55db2b43451086dd75e8f2b0cf Mon Sep 17 00:00:00 2001 From: Chenglong Wang Date: Sat, 1 Aug 2026 09:30:11 -0700 Subject: [PATCH 062/164] playground: add Swiss lab with hand-authored Vega-Lite mockups Manual International Typographic Style reference specs (visible modular grid, flush-left Helvetica title, warm paper + signal-red accent, hard corners) across bar/line/grouped/scatter/area/hbar. Establishes the look-and-feel target for a future swiss ThemeSpec preset; not pipeline output. Wired into /playground/swiss-lab. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 33e4b998-4b58-4d7e-9cf8-c107b10e45c2 --- site/src/main.tsx | 2 + site/src/playground/PlaygroundShell.tsx | 1 + site/src/playground/SwissLab.tsx | 73 +++++ site/src/playground/swiss-lab-data.ts | 363 ++++++++++++++++++++++++ 4 files changed, 439 insertions(+) create mode 100644 site/src/playground/SwissLab.tsx create mode 100644 site/src/playground/swiss-lab-data.ts diff --git a/site/src/main.tsx b/site/src/main.tsx index ebc9c96e..88e7130e 100644 --- a/site/src/main.tsx +++ b/site/src/main.tsx @@ -19,6 +19,7 @@ import { ThemeLab } from './playground/ThemeLab'; import { ThemeLabR2 } from './playground/ThemeLabR2'; import { ThemeLabReal } from './playground/ThemeLabReal'; import { ThemeLabGaps } from './playground/ThemeLabGaps'; +import { SwissLab } from './playground/SwissLab'; import { FullTestCases } from './playground/FullTestCases'; import { LocaleProvider, useLocale } from './i18n/LocaleContext'; import type { Locale } from './i18n/locales'; @@ -60,6 +61,7 @@ function AppRoutes({ locale }: { locale: Locale }) { } /> } /> } /> + } /> } /> {/* Tutorials merged into Documentation as the "Quick start" group. */} diff --git a/site/src/playground/PlaygroundShell.tsx b/site/src/playground/PlaygroundShell.tsx index 72702e00..edf35f83 100644 --- a/site/src/playground/PlaygroundShell.tsx +++ b/site/src/playground/PlaygroundShell.tsx @@ -11,6 +11,7 @@ const pages = [ { to: 'theme-lab-r2', label: 'Theme lab R2' }, { to: 'theme-lab-real', label: 'Theme lab real' }, { to: 'theme-lab-gaps', label: 'Theme lab gaps' }, + { to: 'swiss-lab', label: 'Swiss lab' }, { to: 'full-test-cases', label: 'Full test cases' }, ]; diff --git a/site/src/playground/SwissLab.tsx b/site/src/playground/SwissLab.tsx new file mode 100644 index 00000000..d4c4e878 --- /dev/null +++ b/site/src/playground/SwissLab.tsx @@ -0,0 +1,73 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** + * Swiss lab — hand-authored Vega-Lite mockups of the International Typographic + * Style, laid out as a simple chart grid for look-and-feel inspection. These + * are manual reference specs (see swiss-lab-data.ts), NOT theme-pipeline output; + * they establish the target a future "swiss" ThemeSpec preset should reproduce. + */ + +import { siteTheme } from '../shared/theme'; +import { VegaLiteView } from '../components/VegaLiteView'; +import { SWISS_CASES } from './swiss-lab-data'; + +export function SwissLab() { + return ( +
+
+

+ Swiss lab · hand-authored mockups +

+

+ Manual Vega-Lite specs in the International Typographic Style — visible modular + grid, flush-left Helvetica title, warm paper + signal-red accent, hard corners. + These are a design target for a future swiss preset, not + theme-pipeline output. Refs: swissted.com · Poster House "The Swiss Grid" · + Müller-Brockmann · Vignelli subway map · Aicher '72 palette. +

+
+ +
+ {SWISS_CASES.map((c) => ( +
+
+ +
+
+
+ {c.title} + + {c.id} + +
+
{c.note}
+
+
+ ))} +
+
+ ); +} diff --git a/site/src/playground/swiss-lab-data.ts b/site/src/playground/swiss-lab-data.ts new file mode 100644 index 00000000..de8f1553 --- /dev/null +++ b/site/src/playground/swiss-lab-data.ts @@ -0,0 +1,363 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** + * Swiss / International Typographic Style — hand-authored Vega-Lite mockups. + * + * These are NOT produced by the theme pipeline. They are manual reference + * specs, written to establish the look & feel of a future "swiss" ThemeSpec + * preset so a human can eyeball the target before we ground it. The design + * tokens they encode (and that the eventual preset must reproduce): + * + * - Type: Helvetica / Akzidenz-Grotesk, flush-left, bold title block. + * - Grid: the modular grid is VISIBLE (unlike NYT, which hides it). + * - Palette: ink black + warm paper + ONE saturated accent (signal red), + * with flat cobalt / mustard / green as extended categoricals. + * - Furniture: hard square corners, thick baseline/domain rules, no shadows, + * no rounding, generous structured margins. + * - Mood: objective, high-contrast, systematic. + * + * Refs: swissted.com · Poster House "The Swiss Grid" · Müller-Brockmann + * Tonhalle posters · Vignelli 1972 NYC Subway Map · Aicher 1972 Munich palette. + */ + +const FONT = '"Helvetica Neue", Helvetica, Arial, sans-serif'; +const INK = '#1a1a1a'; +const PAPER = '#f4f1ea'; +const GRID = '#d9d5cc'; +const RED = '#e2231a'; + +/** Signal-red first, then flat cobalt / mustard / forest / slate. */ +export const SWISS_PALETTE = [RED, INK, '#0067a5', '#f2b705', '#2a7f4f', '#6b6b6b']; + +/** Shared config — the "system". Every spec spreads this so the grid, type + * and rules stay identical across chart types (that consistency IS the style). */ +const swissConfig = { + background: PAPER, + font: FONT, + padding: { left: 16, top: 14, right: 16, bottom: 14 }, + title: { + anchor: 'start', + font: FONT, + fontSize: 16, + fontWeight: 700, + color: INK, + subtitleFont: FONT, + subtitleFontSize: 11.5, + subtitleColor: '#666', + subtitlePadding: 6, + offset: 14, + }, + view: { stroke: null }, + axis: { + domain: true, + domainColor: INK, + domainWidth: 1.5, + grid: true, + gridColor: GRID, + gridWidth: 1, + tickColor: INK, + tickWidth: 1.5, + tickSize: 6, + labelFont: FONT, + labelFontSize: 11, + labelColor: INK, + labelPadding: 4, + titleFont: FONT, + titleFontSize: 11.5, + titleFontWeight: 700, + titleColor: INK, + }, + legend: { + orient: 'top', + direction: 'horizontal', + titleFont: FONT, + titleColor: INK, + titleFontSize: 11, + titleFontWeight: 700, + labelFont: FONT, + labelFontSize: 11, + labelColor: INK, + symbolType: 'square', + symbolSize: 90, + offset: 6, + padding: 0, + }, +}; + +export interface SwissCase { + id: string; + title: string; + note: string; + spec: any; +} + +const W = 360; +const H = 300; + +export const SWISS_CASES: SwissCase[] = [ + // ── 1. Vertical bars — the archetypal Swiss chart. Flat red, value grid. ── + { + id: 'swiss-bar', + title: 'Vertical bars', + note: 'Single accent (signal red), visible value grid, flush-left bold title, hard corners.', + spec: { + width: W, + height: H, + background: PAPER, + title: { text: 'Output by sector', subtitle: 'Index, 2024 (2015 = 100)' }, + data: { + values: [ + { sector: 'Manufacturing', value: 128 }, + { sector: 'Services', value: 141 }, + { sector: 'Construction', value: 96 }, + { sector: 'Agriculture', value: 74 }, + { sector: 'Energy', value: 112 }, + { sector: 'Transport', value: 103 }, + ], + }, + mark: { type: 'bar', color: RED, cornerRadius: 0 }, + encoding: { + x: { + field: 'sector', + type: 'nominal', + sort: null, + axis: { labelAngle: 0, grid: false, title: null, labelFontSize: 10 }, + }, + y: { + field: 'value', + type: 'quantitative', + axis: { title: 'Index', tickCount: 5 }, + }, + }, + config: swissConfig, + }, + }, + + // ── 2. Multi-series line — straight linear segments, ink + red + cobalt. ── + { + id: 'swiss-line', + title: 'Line series', + note: 'Straight linear segments (no smoothing), thick strokes, square legend swatches on top.', + spec: { + width: W, + height: H, + background: PAPER, + title: { text: 'Real wages by region', subtitle: 'Index, 2010–2024 (2010 = 100)' }, + data: { + values: (() => { + const rows: any[] = []; + const series: Record = { + North: [100, 103, 107, 111, 116, 121, 124, 129], + South: [100, 101, 99, 102, 105, 104, 108, 111], + West: [100, 98, 97, 100, 103, 109, 115, 122], + }; + const years = [2010, 2012, 2014, 2016, 2018, 2020, 2022, 2024]; + for (const [region, vals] of Object.entries(series)) + vals.forEach((v, i) => rows.push({ region, year: years[i], value: v })); + return rows; + })(), + }, + mark: { type: 'line', strokeWidth: 3, interpolate: 'linear', point: false }, + encoding: { + x: { + field: 'year', + type: 'quantitative', + axis: { format: 'd', title: null, tickCount: 4, grid: false }, + scale: { nice: false }, + }, + y: { field: 'value', type: 'quantitative', axis: { title: 'Index' } }, + color: { + field: 'region', + type: 'nominal', + scale: { range: SWISS_PALETTE }, + legend: { title: null }, + }, + }, + config: swissConfig, + }, + }, + + // ── 3. Grouped bars — categorical comparison, flat palette, hard modules. ── + { + id: 'swiss-grouped-bar', + title: 'Grouped bars', + note: 'Flat categorical palette, no gaps rounded, grouped columns read as a grid module.', + spec: { + width: W, + height: H, + background: PAPER, + title: { text: 'Energy mix by country', subtitle: 'Share of generation, 2024 (%)' }, + data: { + values: (() => { + const rows: any[] = []; + const data: Record> = { + Fossil: { CH: 2, DE: 42, FR: 8, IT: 55 }, + Nuclear: { CH: 33, DE: 6, FR: 68, IT: 0 }, + Renewable: { CH: 65, DE: 52, FR: 24, IT: 45 }, + }; + for (const [source, byC] of Object.entries(data)) + for (const [country, v] of Object.entries(byC)) + rows.push({ source, country, value: v }); + return rows; + })(), + }, + mark: { type: 'bar', cornerRadius: 0 }, + encoding: { + x: { + field: 'country', + type: 'nominal', + axis: { labelAngle: 0, grid: false, title: null }, + }, + xOffset: { field: 'source' }, + y: { field: 'value', type: 'quantitative', axis: { title: 'Share (%)' } }, + color: { + field: 'source', + type: 'nominal', + scale: { range: SWISS_PALETTE }, + legend: { title: null }, + }, + }, + config: swissConfig, + }, + }, + + // ── 4. Scatter — geometric square marks on a full modular grid. ── + { + id: 'swiss-scatter', + title: 'Scatter', + note: 'Square marks (geometric), both axes gridded → the modular grid is the picture.', + spec: { + width: W, + height: H, + background: PAPER, + title: { text: 'Density vs. rent', subtitle: '48 districts, 2024' }, + data: { + values: (() => { + // Deterministic pseudo-random cloud with a mild positive trend. + const rows: any[] = []; + let s = 7; + const rnd = () => ((s = (s * 9301 + 49297) % 233280) / 233280); + for (let i = 0; i < 48; i++) { + const d = 20 + rnd() * 160; + rows.push({ density: d, rent: 8 + d * 0.09 + (rnd() - 0.5) * 9 }); + } + return rows; + })(), + }, + mark: { + type: 'point', + shape: 'square', + filled: true, + size: 70, + fill: RED, + fillOpacity: 0.85, + stroke: INK, + strokeWidth: 0.6, + }, + encoding: { + x: { + field: 'density', + type: 'quantitative', + axis: { title: 'Density (per ha)' }, + scale: { nice: true }, + }, + y: { + field: 'rent', + type: 'quantitative', + axis: { title: 'Rent (CHF/m²)' }, + scale: { nice: true }, + }, + }, + config: swissConfig, + }, + }, + + // ── 5. Stacked area — two flat fills, ink + red, straight segments. ── + { + id: 'swiss-area', + title: 'Stacked area', + note: 'Flat solid fills (no gradients), straight segments, value grid over the stack.', + spec: { + width: W, + height: H, + background: PAPER, + title: { text: 'Ridership by line', subtitle: 'Million trips per year' }, + data: { + values: (() => { + const rows: any[] = []; + const years = [2018, 2019, 2020, 2021, 2022, 2023, 2024]; + const tram = [42, 44, 28, 33, 41, 46, 49]; + const bus = [31, 32, 22, 25, 29, 33, 35]; + years.forEach((y, i) => { + rows.push({ year: y, line: 'Tram', trips: tram[i] }); + rows.push({ year: y, line: 'Bus', trips: bus[i] }); + }); + return rows; + })(), + }, + mark: { type: 'area', interpolate: 'linear', line: { strokeWidth: 1 } }, + encoding: { + x: { + field: 'year', + type: 'quantitative', + axis: { format: 'd', title: null, tickCount: 4, grid: false }, + scale: { nice: false }, + }, + y: { + field: 'trips', + type: 'quantitative', + stack: 'zero', + axis: { title: 'Trips (M)' }, + }, + color: { + field: 'line', + type: 'nominal', + scale: { range: SWISS_PALETTE }, + legend: { title: null }, + }, + }, + config: swissConfig, + }, + }, + + // ── 6. Horizontal ranking — flush-left category labels, sorted, red bars. ── + { + id: 'swiss-hbar', + title: 'Horizontal ranking', + note: 'Flush-left category labels, sorted descending, single accent, value grid on x.', + spec: { + width: W, + height: H, + background: PAPER, + title: { text: 'Cost of a coffee', subtitle: 'City average, CHF' }, + data: { + values: [ + { city: 'Zürich', value: 4.8 }, + { city: 'Geneva', value: 4.6 }, + { city: 'Copenhagen', value: 4.3 }, + { city: 'Oslo', value: 4.1 }, + { city: 'Stockholm', value: 3.7 }, + { city: 'Vienna', value: 3.2 }, + { city: 'Berlin', value: 3.0 }, + { city: 'Lisbon', value: 1.9 }, + ], + }, + mark: { type: 'bar', color: RED, cornerRadius: 0 }, + encoding: { + y: { + field: 'city', + type: 'nominal', + sort: '-x', + axis: { title: null, grid: false, labelFontSize: 11, domain: false, ticks: false }, + }, + x: { + field: 'value', + type: 'quantitative', + axis: { title: 'CHF', tickCount: 5 }, + }, + }, + config: swissConfig, + }, + }, +]; From e97f078898e39db58410e5ca6c4117b0cbd30f36 Mon Sep 17 00:00:00 2001 From: Chenglong Wang Date: Sat, 1 Aug 2026 09:50:49 -0700 Subject: [PATCH 063/164] theme: add Swiss (International Typographic Style) preset MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Grounds the Swiss-lab mockups into a shippable house: warm paper, a visible modular grid (drawn, not hidden), black structural axes under a bold flush-left Helvetica headline sitting over a thick header rule, and a single signal-red accent with flat primaries for extra categories. Signed data (waterfall steps, diverging tables) routes through status ink — cobalt up, red down, neutral total — via selection.signed. Wired into THEME_PRESETS; appears as a column in the R2/audit/real theme labs automatically. Gates green: flint tsc, 696 vitest, r2 0/85, theme-audit clean, site tsc + build. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 33e4b998-4b58-4d7e-9cf8-c107b10e45c2 --- packages/flint-js/src/core/theme/presets.ts | 2 + .../flint-js/src/core/theme/presets/swiss.ts | 209 ++++++++++++++++++ 2 files changed, 211 insertions(+) create mode 100644 packages/flint-js/src/core/theme/presets/swiss.ts diff --git a/packages/flint-js/src/core/theme/presets.ts b/packages/flint-js/src/core/theme/presets.ts index e47ff428..b9a5ae25 100644 --- a/packages/flint-js/src/core/theme/presets.ts +++ b/packages/flint-js/src/core/theme/presets.ts @@ -17,6 +17,7 @@ import { mckinsey } from './presets/mckinsey'; import { datawrapper } from './presets/datawrapper'; import { powerbi } from './presets/powerbi'; import { powerbiLight } from './presets/powerbi-light'; +import { swiss } from './presets/swiss'; export const THEME_PRESETS: Record = { nyt, @@ -26,6 +27,7 @@ export const THEME_PRESETS: Record = { datawrapper, powerbi, 'powerbi-light': powerbiLight, + swiss, }; /** The catalogue, without the specs — enough to choose by. */ diff --git a/packages/flint-js/src/core/theme/presets/swiss.ts b/packages/flint-js/src/core/theme/presets/swiss.ts new file mode 100644 index 00000000..b5db10e8 --- /dev/null +++ b/packages/flint-js/src/core/theme/presets/swiss.ts @@ -0,0 +1,209 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import type { ThemePreset } from '../types'; + +/** + * Swiss / International Typographic Style. + * + * Modelled on the hand-authored mockups in the Swiss lab (see + * `site/src/playground/swiss-lab-data.ts`), which were in turn measured against + * the poster/diagram canon — swissted.com, Poster House "The Swiss Grid", + * Müller-Brockmann's Tonhalle posters, Vignelli's 1972 NYC subway diagram, and + * Otl Aicher's 1972 Munich colour system. + * + * What makes it a different design *language* rather than a palette reskin of + * the editorial houses: the modular grid is drawn, not hidden (NYT hides it); + * the axes are structural black rules; the headline is a bold flush-left + * Helvetica block sitting over a thick rule; and colour is a single saturated + * signal red against warm paper, with flat primaries for extra categories. No + * gradients on the marks, no rounded corners, no ornament. + */ +export const swiss: ThemePreset = { + id: 'swiss', + label: 'Swiss', + description: + 'International Typographic Style: warm paper, a visible modular grid, black structural axes, a bold flush-left Helvetica headline over a rule, and a single signal-red accent.', + guidance: [ + '- `title` carries the naming in a bold flush-left block; `subtitle` names the measure and period.', + '- Annotate the measure with `unit` in `semantic_types`.', + '- Colour is a single signal red; the categorical key tells 5 series apart.', + ].join('\n'), + spec: { + id: 'swiss', + label: 'Swiss', + ink: { + surface: { + source: 'house', + canvas: '#f4f1ea', + plot: '#f4f1ea', + }, + text: { + primary: '#1a1a1a', + secondary: '#555555', + muted: '#8a8a8a', + }, + structure: { + grid: '#d9d5cc', + axis: '#1a1a1a', + rule: '#1a1a1a', + connector: '#8a8a8a', + }, + series: { + single: '#e2231a', + categorical: ['#e2231a', '#1a1a1a', '#0067a5', '#f2b705', '#2a7f4f'], + categoricalExtended: [ + '#e2231a', + '#1a1a1a', + '#0067a5', + '#f2b705', + '#2a7f4f', + '#e06d1f', + '#5b4b8a', + '#1f8a8a', + '#b5195f', + '#8a8a2a', + '#5a6b78', + '#8a5a2a', + ], + // Sequential: a single-hue red ramp, binned into steps — a scale + // the reader can name a bin off, not a wash. + sequential: { + stops: ['#fbe3df', '#f4a19a', '#eb6a5f', '#e2231a', '#9e120c'], + space: 'lab', + endpointsAgainstSurface: true, + consumption: 'quantize', + quantizeCount: 5, + }, + // Diverging: cobalt to signal red, through the paper neutral. + diverging: { + stops: ['#0067a5', '#7fb2d6', '#efeae0', '#ef9a90', '#e2231a'], + neutral: '#efeae0', + space: 'lab', + endpointsAgainstSurface: true, + consumption: 'quantize', + quantizeCount: 5, + }, + // Signed data reads as the two Swiss primaries: cobalt up, + // signal red down, a neutral grey for the anchoring total. + status: { + positive: '#0067a5', + negative: '#e2231a', + neutral: '#9a9a9a', + }, + overflow: '#9a9a9a', + selection: { + signed: 'status', + statusUse: 'anySigned', + }, + }, + accent: '#e2231a', + }, + type: { + minSize: 9, + headline: { + family: "'Helvetica Neue', Helvetica, Arial, sans-serif", + size: 'text.400', + weight: 'bold', + }, + deck: { + size: 'text.200', + color: '#555555', + }, + axisLabel: { + size: 'text.100', + }, + axisTitle: { + size: 'text.100', + weight: 'bold', + color: '#1a1a1a', + }, + }, + structure: { + axis: { + categorical: { + line: 'full', + ticks: 'omit', + }, + measure: { + line: 'full', + ticks: 'full', + tickLength: 'short', + tickDirection: 'outward', + }, + }, + grid: { + measure: 'quiet', + category: 'omit', + style: 'solid', + zero: 'full', + }, + frame: 'omit', + baseline: 'full', + }, + marks: { + bandFraction: 0.7, + strokeWeight: 3, + strokeCap: 'butt', + strokeJoin: 'miter', + interpolation: 'linear', + point: { + presence: 'omit', + fill: 'solid', + }, + separator: { + presence: 'hairline', + source: 'surface', + width: 1.5, + }, + slice: { + gap: 2, + gapStyle: 'rule', + }, + sizeRange: [12, 400], + }, + labels: { + truncation: 'never', + flush: true, + angle: 'auto', + }, + legend: { + show: 'always', + placement: ['top'], + direction: 'horizontal', + title: 'omit', + suppressWhenAxisNames: true, + }, + dataLabels: { + show: 'whenTheyFit', + placement: 'outsideMark', + inkMode: 'fixed', + }, + annotation: { + axisTitles: 'whenAmbiguous', + unit: 'lastTick', + numberFormat: { + precision: 'auto', + }, + }, + furniture: [ + { + kind: 'headerRule', + anchor: 'topLeft', + color: '#1a1a1a', + height: 3, + }, + ], + layout: { + density: 'normal', + targetWidth: 300, + titleBlock: { + anchor: 'start', + gap: 'normal', + }, + }, + compileDefaults: { + baseSize: { width: 420, height: 320 }, + }, + }, +}; From 13771f706680b2c63be8348a0d1c0667e029f76d Mon Sep 17 00:00:00 2001 From: Chenglong Wang Date: Sat, 1 Aug 2026 09:58:09 -0700 Subject: [PATCH 064/164] theme: drop the Swiss header rule It read as floating chrome above the plot and was never in the approved Swiss-lab mockups. The bold flush-left headline over warm paper carries the block on its own. Gates: flint tsc, theme vitest, r2 0/85. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 33e4b998-4b58-4d7e-9cf8-c107b10e45c2 --- packages/flint-js/src/core/theme/presets/swiss.ts | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/packages/flint-js/src/core/theme/presets/swiss.ts b/packages/flint-js/src/core/theme/presets/swiss.ts index b5db10e8..1b14fc39 100644 --- a/packages/flint-js/src/core/theme/presets/swiss.ts +++ b/packages/flint-js/src/core/theme/presets/swiss.ts @@ -15,9 +15,9 @@ import type { ThemePreset } from '../types'; * What makes it a different design *language* rather than a palette reskin of * the editorial houses: the modular grid is drawn, not hidden (NYT hides it); * the axes are structural black rules; the headline is a bold flush-left - * Helvetica block sitting over a thick rule; and colour is a single saturated - * signal red against warm paper, with flat primaries for extra categories. No - * gradients on the marks, no rounded corners, no ornament. + * Helvetica block; and colour is a single saturated signal red against warm + * paper, with flat primaries for extra categories. No gradients on the marks, + * no rounded corners, no ornament. */ export const swiss: ThemePreset = { id: 'swiss', @@ -186,14 +186,6 @@ export const swiss: ThemePreset = { precision: 'auto', }, }, - furniture: [ - { - kind: 'headerRule', - anchor: 'topLeft', - color: '#1a1a1a', - height: 3, - }, - ], layout: { density: 'normal', targetWidth: 300, From 47f5a119f8e23ca63b26c90b12e2db641abec374 Mon Sep 17 00:00:00 2001 From: Chenglong Wang Date: Sat, 1 Aug 2026 10:11:36 -0700 Subject: [PATCH 065/164] theme: keep the band baseline continuous under bar edge strokes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A house that strokes its bars in the surface colour (datawrapper, swiss) draws that stroke on the bar's edge — which straddles the category axis line at the base and chops it into a dash under every bar. The baseline is furniture the marks rest on, so draw it last: when the separator is applied, raise the band axis over the marks (zindex 1). Only the band axis, and only for houses that stroke their bars — the value axis and unstroked houses keep the renderer's order. Fixes the broken x-axis on plain, stacked, grouped, horizontal, and waterfall bars for both datawrapper and swiss. Gates: flint tsc, 696 vitest, r2 0/85. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 33e4b998-4b58-4d7e-9cf8-c107b10e45c2 --- packages/flint-js/src/vegalite/theme.ts | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/packages/flint-js/src/vegalite/theme.ts b/packages/flint-js/src/vegalite/theme.ts index 4cbacded..c3bf25e0 100644 --- a/packages/flint-js/src/vegalite/theme.ts +++ b/packages/flint-js/src/vegalite/theme.ts @@ -1168,6 +1168,7 @@ function applyMarks(spec: any, d: DesignDecisions, table: any[], say: (p: string const plotW = spec.config?.view?.continuousWidth ?? spec._width ?? 300; const plotH = spec.config?.view?.continuousHeight ?? spec._height ?? 300; let saidThin = false; + let saidBaseline = false; walk(spec, (node) => { const mark = markTypeOf(node.mark); if (mark !== 'bar' && mark !== 'rect') return; @@ -1191,6 +1192,26 @@ function applyMarks(spec: any, d: DesignDecisions, table: any[], say: (p: string return; } node.mark = { ...normalizeMark(node.mark), stroke: m.separator!.color, strokeWidth: m.separator!.width }; + // A bar sits *on* the category baseline, so its edge stroke straddles + // the axis line at the base and — painted in the surface — chops it + // into a dash under every bar. The baseline is furniture the marks + // rest on, so it is drawn last: raise the band axis over the marks + // and the rule reads continuous again. Only the band axis, and only + // where a house strokes its bars — the value axis and unstroked + // houses keep the renderer's order. + const enc = node.encoding ?? {}; + const bandCh = (['x', 'y'] as const).find((ch) => { + const e = enc[ch]; + return e?.field && e.type !== 'quantitative'; + }); + if (bandCh) { + enc[bandCh] = { ...enc[bandCh], axis: { ...(enc[bandCh].axis ?? {}), zindex: 1 } }; + if (!saidBaseline) { + say('marks.separator', + 'the band axis is drawn over the bars so its baseline reads continuous under the edge strokes'); + saidBaseline = true; + } + } }); } From 8b45a77606c58f469e0c6787ea85b97720e496a9 Mon Sep 17 00:00:00 2001 From: Chenglong Wang Date: Sat, 1 Aug 2026 10:20:10 -0700 Subject: [PATCH 066/164] theme: give McKinsey lollipop stems a solid connector ink MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The stem borrowed McKinsey's rule ink where no connector was named, but that rule is a near-white gridline (#d3dce1) — far too faint to carry the eye up to the dot, so stems all but vanished. McKinsey's own exhibits draw the stem a solid medium-light grey, clearly present yet subordinate to the near-black dot. Declare that as the house's structure.connector ink. Also firms up dumbbell spans and waterfall bridges (same ink) without overpowering them. Gates: flint tsc, 44 theme vitest, r2 0/85. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 33e4b998-4b58-4d7e-9cf8-c107b10e45c2 --- packages/flint-js/src/core/theme/presets/mckinsey.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/flint-js/src/core/theme/presets/mckinsey.ts b/packages/flint-js/src/core/theme/presets/mckinsey.ts index 6e9dc0f1..4c56acec 100644 --- a/packages/flint-js/src/core/theme/presets/mckinsey.ts +++ b/packages/flint-js/src/core/theme/presets/mckinsey.ts @@ -30,7 +30,14 @@ export const mckinsey: ThemePreset = { }, "structure": { "axis": "#051c2c", - "rule": "#d3dce1" + "rule": "#d3dce1", + // A lollipop stem borrows the rule where no connector ink is + // named, but McKinsey's rule is a near-white gridline (#d3dce1) + // — far too faint for a stem that must carry the eye up to the + // dot. Their exhibits draw the stem a solid medium-light grey, + // clearly present yet subordinate to the near-black dot. So the + // connector states its own, more definite ink. + "connector": "#a7b1bc" }, "series": { // McKinsey's 2020 "Deep Blue" (#051c2c) is deliberately From aea55bdc4b5ccc425555c2d59d1fc14c92882068 Mon Sep 17 00:00:00 2001 From: Chenglong Wang Date: Sat, 1 Aug 2026 10:57:06 -0700 Subject: [PATCH 067/164] theme: redraw the category baseline over stroked bars; widen lollipop ends MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two separator/point-scale fixes in the realizer: 1. Baseline chop. A house that strokes its bars in the surface colour (datawrapper white, swiss paper) draws that stroke on every edge, including the bottom edge that sits on the category axis — chopping the domain line into a dash under each bar. Raising the axis over the marks does not work: Vega-Lite drops zindex off a config axis, so it stays behind. Instead the baseline is redrawn as its own rule on top of the bars (as the zero rule already is), at the measure's zero in the band axis's domain ink. The measure axis is read off the bar so a waterfall, which carries its category on the parent layer, is handled too; the band channel is nulled on the rule so it spans full width without inheriting a shared category (which had added an 'undefined' band). Houses that draw no band domain, and table charts whose category axis is a header gutter, are skipped. Fixes plain, stacked, grouped, horizontal and waterfall bars for datawrapper and swiss. 2. Lollipop end gap. A lollipop's category axis is a point scale, so its zero-width dots sat only half a step from the axis (default pointPadding 0.5) while every neighbour sat a full step apart. The end dots now stand a full step off the spine (pointPadding 1) so they are not cramped. Gates: flint tsc, 696 vitest, site tsc, r2 0/85, theme-audit clean. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 33e4b998-4b58-4d7e-9cf8-c107b10e45c2 --- packages/flint-js/src/vegalite/theme.ts | 108 +++++++++++++++++++----- 1 file changed, 87 insertions(+), 21 deletions(-) diff --git a/packages/flint-js/src/vegalite/theme.ts b/packages/flint-js/src/vegalite/theme.ts index c3bf25e0..9deaa8b5 100644 --- a/packages/flint-js/src/vegalite/theme.ts +++ b/packages/flint-js/src/vegalite/theme.ts @@ -1168,7 +1168,8 @@ function applyMarks(spec: any, d: DesignDecisions, table: any[], say: (p: string const plotW = spec.config?.view?.continuousWidth ?? spec._width ?? 300; const plotH = spec.config?.view?.continuousHeight ?? spec._height ?? 300; let saidThin = false; - let saidBaseline = false; + let sawStroke = false; + let measureCh: 'x' | 'y' | undefined; walk(spec, (node) => { const mark = markTypeOf(node.mark); if (mark !== 'bar' && mark !== 'rect') return; @@ -1192,27 +1193,18 @@ function applyMarks(spec: any, d: DesignDecisions, table: any[], say: (p: string return; } node.mark = { ...normalizeMark(node.mark), stroke: m.separator!.color, strokeWidth: m.separator!.width }; - // A bar sits *on* the category baseline, so its edge stroke straddles - // the axis line at the base and — painted in the surface — chops it - // into a dash under every bar. The baseline is furniture the marks - // rest on, so it is drawn last: raise the band axis over the marks - // and the rule reads continuous again. Only the band axis, and only - // where a house strokes its bars — the value axis and unstroked - // houses keep the renderer's order. + sawStroke = true; + // Which axis carries the measure is read off the bar, not off where + // the encoding sits: a waterfall carries its category on the parent + // layer and only the measure on the bar. The measure is the + // quantitative channel (or the one that spans, x2/y2). const enc = node.encoding ?? {}; - const bandCh = (['x', 'y'] as const).find((ch) => { - const e = enc[ch]; - return e?.field && e.type !== 'quantitative'; - }); - if (bandCh) { - enc[bandCh] = { ...enc[bandCh], axis: { ...(enc[bandCh].axis ?? {}), zindex: 1 } }; - if (!saidBaseline) { - say('marks.separator', - 'the band axis is drawn over the bars so its baseline reads continuous under the edge strokes'); - saidBaseline = true; - } - } + if (enc.y?.type === 'quantitative' || enc.y2) measureCh = 'y'; + else if (enc.x?.type === 'quantitative' || enc.x2) measureCh = 'x'; + else if (enc.x?.field && enc.x.type !== 'quantitative') measureCh = 'y'; + else if (enc.y?.field && enc.y.type !== 'quantitative') measureCh = 'x'; }); + if (sawStroke && measureCh) restoreBaseline(spec, measureCh, say); } if (m.tile) applyTileGap(spec, m.tile, say); @@ -1220,6 +1212,55 @@ function applyMarks(spec: any, d: DesignDecisions, table: any[], say: (p: string if (m.slice) applySliceGap(spec, m.slice, table, say); } +/** + * A house that strokes its bars in the surface colour (datawrapper's white + * hairline between stacked segments, swiss's paper gap) draws that stroke on + * every edge of the bar — including the bottom edge, which sits *on* the + * category baseline. Painted in the surface, it chops the axis domain line + * into a dash under each bar. Raising the axis over the marks would fix it, + * but Vega-Lite drops `zindex` off a config axis, so the domain stays behind. + * + * The baseline is furniture the bars rest on, so it is redrawn as its own rule + * on top of them — the same way the zero rule is drawn last. It runs at the + * measure's zero (where an all-positive bar meets the axis) in the band axis's + * own domain ink, so it reads as the one continuous line it always was. A + * house that draws no band domain has nothing to protect, so nothing is added; + * a diverging chart already draws its zero rule on top, and this rule lands on + * the same line. + */ +function restoreBaseline(spec: any, measureCh: 'x' | 'y', say: (p: string, m: string) => void): void { + const bandCh = measureCh === 'y' ? 'x' : 'y'; + const axisCfg = spec.config?.[bandCh === 'x' ? 'axisX' : 'axisY'] ?? {}; + const color = axisCfg.domainColor; + const width = axisCfg.domainWidth ?? 1; + if (axisCfg.domain === false || !color || color === 'transparent' || width <= 0) return; + let said = false; + for (const body of plotBodies(spec)) { + // Only a body that actually plots the measure gets a baseline — a + // furniture rect or a header band carries no measured axis to chop. + const layers: any[] = body.layer ?? [body]; + const carries = layers.some((l) => { + const e = l.encoding?.[measureCh]; + return e?.field && e.type !== 'nominal' && e.type !== 'ordinal'; + }); + if (!carries) continue; + appendLayer(body, { + data: { values: [{}] }, + mark: { type: 'rule', color, strokeWidth: width }, + // Null the band channel so the rule spans the full plot and does + // not inherit a shared category encoding (a waterfall binds `x` on + // the parent layer; without this the rule's empty datum would add + // an "undefined" band to the axis). + encoding: { [measureCh]: { datum: 0 }, [bandCh]: null }, + }); + if (!said) { + say('marks.separator', + 'the category baseline is redrawn over the bars so its edge strokes do not chop it into a dash'); + said = true; + } + } +} + /** * A cell of a grid — a heatmap, a calendar, a matrix. Both of its axes are * spent on position, so unlike a bar it has no free axis to be thinned along: @@ -1299,7 +1340,9 @@ function applyConnectors(spec: any, d: DesignDecisions, say: (p: string, m: stri if (!c?.show) return; const said = new Set(); + let sawStem = false; const paint = (node: any, role: 'stem' | 'bridge' | 'lead'): void => { + if (role === 'stem') sawStem = true; const mark = normalizeMark(node.mark); if (c.color) mark.color = c.color; mark.strokeWidth = role === 'bridge' ? c.spanWidth : c.width; @@ -1351,7 +1394,30 @@ function applyConnectors(spec: any, d: DesignDecisions, say: (p: string, m: stri if (node.facet?.spec) bandWalk(node.facet.spec, enc); }; bandWalk(spec, undefined); -} + + // A lollipop's category axis is a point scale — its dots have no width, so + // Vega-Lite's default `pointPadding` of 0.5 leaves the first and last dot + // only half a step from the axis while every neighbour sits a full step + // apart. On a bar that half-step is right (a bar fills toward the edge); + // on a zero-width dot it reads as cramped against the spine. Widen the + // outer padding so the end dots stand off the axis by the same gap they + // keep from each other. Scoped to a chart that actually hangs stems, and + // to the point scale only (the measured axis is linear), so nothing else + // moves; a house or template that pinned its own padding keeps it. + if (sawStem) { + const config = (spec.config ??= {}); + const scale = (config.scale ??= {}); + if (scale.pointPadding == null) { + scale.pointPadding = LOLLIPOP_CATEGORY_PADDING; + say('config.scale.pointPadding', + `the lollipop's end dots are stood off the category axis by a full step (pointPadding ${LOLLIPOP_CATEGORY_PADDING}) so they are not cramped against the spine`); + } + } +} + +/** Outer padding for a lollipop's category point scale: end dots stand a full + * inter-item gap off the axis instead of Vega-Lite's default half-step. */ +const LOLLIPOP_CATEGORY_PADDING = 1; /** * How far a mark reaches from the point it is anchored at. Only the round From 9756611fa15d0d0a435228e7c43bdd6c585fd143 Mon Sep 17 00:00:00 2001 From: Chenglong Wang Date: Sat, 1 Aug 2026 11:52:19 -0700 Subject: [PATCH 068/164] theme: draw the Economist masthead tab over the title, not under it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Economist's signature is a small red rule at the very top-left of the graphic, over the headline. The tab was declared (furniture mastheadTab) but the realizer drew every top furniture item in a vconcat below the outer view's title, so the red rule sat between the deck and the plot — the opposite of where it belongs. Vega-Lite always renders an outer view's title above every concatenated child, so a tab can never sit over an outer title. applyFurniture now separates a masthead tab from a rule: when a tab opens the block the title rides down onto the plot body (inner.title) and the tab is first in the stack — tab, headline, deck, plot. A header/footer rule still closes its block where it is drawn, so nothing else moves. Verified on plain bars, a legended 5-series stack, and a facet across the Economist column; datawrapper's footer rule and every other house are untouched. Regression test locks the tab-first / title-on-body order. Gates: flint+site tsc, 697 vitest (+1), r2 0/85. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 33e4b998-4b58-4d7e-9cf8-c107b10e45c2 --- packages/flint-js/src/vegalite/theme.ts | 22 ++++++++-- packages/flint-js/tests/theme-titles.test.ts | 43 ++++++++++++++++++++ 2 files changed, 61 insertions(+), 4 deletions(-) diff --git a/packages/flint-js/src/vegalite/theme.ts b/packages/flint-js/src/vegalite/theme.ts index 9deaa8b5..c867c344 100644 --- a/packages/flint-js/src/vegalite/theme.ts +++ b/packages/flint-js/src/vegalite/theme.ts @@ -3923,6 +3923,11 @@ function applyFurniture(spec: any, d: DesignDecisions, table: any[], say: (p: st return false; } + // A masthead tab opens the block *above* the headline — the Economist's + // red rule sits at the very top of the graphic, over the title, not + // between the title and the plot. A rule (header/footer) closes a block + // and stays where it is drawn: under the headline, or under the plot. + const aboveTitle: any[] = []; const before: any[] = []; const after: any[] = []; // A tab is a mark of its own — the Economist's red rectangle is 26px @@ -3949,13 +3954,22 @@ function applyFurniture(spec: any, d: DesignDecisions, table: any[], say: (p: st height: item.height ?? 2, data: { values: [{}] }, }; - (isTop ? before : after).push(rect); + if (!isTop) after.push(rect); + else if (item.kind === 'mastheadTab') aboveTitle.push(rect); + else before.push(rect); } - if (!before.length && !after.length) return false; + if (!aboveTitle.length && !before.length && !after.length) return false; const inner: any = { ...spec }; for (const key of ['$schema', 'background', 'padding', 'title', 'config', 'autosize']) delete inner[key]; + // The masthead tab must draw *over* the title, but the outer view's title + // always renders above every concatenated child. So when a tab opens the + // block, the title rides down onto the plot body and the tab is the first + // thing in the stack — tab, then headline, then chart. + const tabOverTitle = aboveTitle.length > 0 && !!spec.title; + if (tabOverTitle) inner.title = spec.title; + // A concatenated child's legends are hoisted to the outer view and drawn // above every child in it — including the tab, which is the one thing that // is supposed to open the block. Resolved independently they stay with the @@ -3973,9 +3987,9 @@ function applyFurniture(spec: any, d: DesignDecisions, table: any[], say: (p: st ...(spec.$schema ? { $schema: spec.$schema } : {}), background: spec.background, padding: spec.padding, - ...(spec.title ? { title: spec.title } : {}), + ...(spec.title && !tabOverTitle ? { title: spec.title } : {}), spacing: 6, - vconcat: [...before, inner, ...after], + vconcat: [...aboveTitle, ...before, inner, ...after], ...(keyed.size ? { resolve: { legend: Object.fromEntries([...keyed].map((c) => [c, 'independent'])) } } : {}), diff --git a/packages/flint-js/tests/theme-titles.test.ts b/packages/flint-js/tests/theme-titles.test.ts index 5f1b44a7..6d74c0fd 100644 --- a/packages/flint-js/tests/theme-titles.test.ts +++ b/packages/flint-js/tests/theme-titles.test.ts @@ -107,3 +107,46 @@ describe('legend titles', () => { expect(spec._theme.decisions.legend.title).toBe(true); }); }); + +/** + * A masthead tab opens the block above the headline. + * + * The Economist's red rule sits at the very top of the graphic, over the + * title — not between the title and the plot. So a house that draws one keeps + * the tab first in the stack and lets the headline ride down onto the plot. + */ +describe('masthead furniture', () => { + function withTab(): any { + const themeSpec = { + id: 'tabbed', + label: 'Tabbed', + ink: { surface: { canvas: '#ffffff' }, series: { single: '#333333' } }, + annotation: { axisTitles: 'always' }, + furniture: [{ kind: 'mastheadTab', anchor: 'topLeft', color: '#e3120b', width: 26, height: 3 }], + } as unknown as ThemeSpec; + return assembleVegaLite({ + data: { values: MONTHLY }, + semantic_types: { Month: 'Month', Rainfall: 'Amount' }, + chart_spec: { + chartType: 'Bar Chart', + title: 'Rain keeps falling', + encodings: { x: 'Month', y: 'Rainfall' }, + }, + theme_spec: themeSpec, + } as any) as any; + } + + it('draws the tab first and above the title', () => { + const spec = withTab(); + expect(Array.isArray(spec.vconcat)).toBe(true); + // The tab opens the stack. + const first = spec.vconcat[0]; + expect(first.__themeSynthetic).toBe(true); + expect(first.mark.type).toBe('rect'); + expect(first.mark.color).toBe('#e3120b'); + // The title has come off the outer view and ridden down onto the plot. + expect(spec.title).toBeUndefined(); + const titled = spec.vconcat.find((v: any) => v && v.title); + expect(titled).toBeTruthy(); + }); +}); From b096de505bd56c2cc97f0227d50ba02f23a44fc4 Mon Sep 17 00:00:00 2001 From: Chenglong Wang Date: Sat, 1 Aug 2026 12:06:26 -0700 Subject: [PATCH 069/164] theme: size a bar-table's legend to the whole table, not just the bar column MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A bar-table is a root hconcat of side-by-side columns — the bars-and-labels panel and the printed value column. A top or bottom legend flows across the whole graphic, but blockWidth (which sizes the legend's column wrap) took the widest single column, so the key wrapped as if only the bar plot existed and crowded into too few columns while the space above the value column sat empty. blockWidth now recognises a row of columns: for a root hconcat it returns the sum of the columns' widths plus their spacing, not the widest one. The bar-table legend now spreads across the full table width and reaches over the value column — Datawrapper/Swiss/NYT/Economist each gain a column and read evenly. Charts that direct-label their series (pyramids, sparkline/radar tables) draw no wrapped top legend and are untouched. Gates: flint+site tsc, 697 vitest, r2 0/85. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 33e4b998-4b58-4d7e-9cf8-c107b10e45c2 --- packages/flint-js/src/vegalite/theme.ts | 27 +++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/packages/flint-js/src/vegalite/theme.ts b/packages/flint-js/src/vegalite/theme.ts index c867c344..565f9c80 100644 --- a/packages/flint-js/src/vegalite/theme.ts +++ b/packages/flint-js/src/vegalite/theme.ts @@ -4014,7 +4014,7 @@ function clamp(v: number, lo: number, hi: number): number { * categories decide — so the total is read back the same way the layout wrote * it, one band per name. */ -function blockWidth(spec: any, table: any[]): number | undefined { +function widestWidth(spec: any, table: any[]): number | undefined { let widest: number | undefined; const consider = (w: number | undefined) => { if (w != null && Number.isFinite(w) && (widest == null || w > widest)) widest = w; @@ -4041,7 +4041,30 @@ function blockWidth(spec: any, table: any[]): number | undefined { // A chart that states no width of its own is drawn at the one the layout // put in the view config — that is the width, not a default to guess at. if (widest == null) consider(spec.config?.view?.continuousWidth); - return widest == null ? undefined : Math.round(widest * 1.07); + return widest; +} + +function blockWidth(spec: any, table: any[]): number | undefined { + const widest = widestWidth(spec, table); + if (widest == null) return undefined; + // A row of columns laid side by side — a bar-table's bars and its printed + // value column — is as wide as the columns *summed*, not as wide as the + // widest one. Anything that flows across the whole graphic (a top or bottom + // legend) has that whole width to spread over, so the key is not crowded + // into the few columns the bar plot alone would allow while the space above + // the value column sits empty. + if (Array.isArray(spec.hconcat) && spec.hconcat.length > 1) { + const spacing = typeof spec.spacing === 'number' ? spec.spacing : 8; + let sum = 0; + let ok = true; + for (const child of spec.hconcat) { + const cw = widestWidth(child, table); + if (cw == null) { ok = false; break; } + sum += cw; + } + if (ok && sum > widest) return Math.round(sum * 1.07 + spacing * (spec.hconcat.length - 1)); + } + return Math.round(widest * 1.07); } void contrastingInk; From cdc6610604c38d4e5719e3543c4fdee85c8c68da Mon Sep 17 00:00:00 2001 From: Chenglong Wang Date: Sat, 1 Aug 2026 12:46:42 -0700 Subject: [PATCH 070/164] theme: lengthen the Economist masthead line to ~1/10 of the chart width MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Economist's red decoration rule is sized relative to the chart — the style guide places it at roughly 1/8 to 1/10 of the graphic's width. Ours was a fixed 26px on a ~460px plot (about 1/18), reading as a stub rather than a rule. Widened to 46px (~1/10 of the base plot), keeping the thin 2-3px rule height. Verified above the headline on plain, stacked, line and faceted Economist charts. Refs: The Economist Visual Styleguide (charting); matplotlib/ggplot Economist recreations. Gates: flint tsc, 45 theme vitest, r2 0/85. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 33e4b998-4b58-4d7e-9cf8-c107b10e45c2 --- packages/flint-js/src/core/theme/presets/economist.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/flint-js/src/core/theme/presets/economist.ts b/packages/flint-js/src/core/theme/presets/economist.ts index d6af8392..51d1b95e 100644 --- a/packages/flint-js/src/core/theme/presets/economist.ts +++ b/packages/flint-js/src/core/theme/presets/economist.ts @@ -161,10 +161,14 @@ export const economist: ThemePreset = { }, "furniture": [ { + // The red rule runs about a tenth of the chart's width — the + // Economist style guide places it at ~1/8–1/10 of the graphic + // (≈46–58px on this ~460px plot). 46 keeps it a thin rule at + // the top-left, not a stub. "kind": "mastheadTab", "anchor": "topLeft", "color": "#e3120b", - "width": 26, + "width": 46, "height": 3 } ], From 5c80262ea3ff6f49f364ab16425ad0eb56fad0a3 Mon Sep 17 00:00:00 2001 From: Chenglong Wang Date: Sat, 1 Aug 2026 14:04:06 -0700 Subject: [PATCH 071/164] Refine Economist theme from the 2017 chart style guide Consumed the Economist visual style guide (2017 v1.2) and applied three faithful refinements to the economist preset: - Red masthead tab is now a chunky ~3:1 block (44x12) rather than a thin 15:1 line. The guide draws it ~15pt wide x 5pt tall (~1/10 of chart width) as a tag over the title, not a hairline. - The zero line is stroked in the Economist red (#e3120b) via a new optional ink.structure.zero (falls back to rule/axis where unset), so every length is read from the signature red baseline when the measure crosses zero. - Categorical palette reordered/refreshed to the guide's colour order: blue, cyan, gold, green, maroon, mauve (was a muted slate/brown-led set), so multi-series charts read as recognisably Economist. Gates: flint tsc, 45 theme vitest, 697 full vitest, r2 0/85. --- packages/flint-js/src/core/theme/ground.ts | 2 +- .../src/core/theme/presets/economist.ts | 24 ++++++++++--------- packages/flint-js/src/core/theme/types.ts | 8 +++++++ 3 files changed, 22 insertions(+), 12 deletions(-) diff --git a/packages/flint-js/src/core/theme/ground.ts b/packages/flint-js/src/core/theme/ground.ts index 9eb469a4..da23618c 100644 --- a/packages/flint-js/src/core/theme/ground.ts +++ b/packages/flint-js/src/core/theme/ground.ts @@ -608,7 +608,7 @@ export function groundTheme(themeIn: ThemeSpec, ctx: GroundingContext): DesignDe // let it be an ordinary line. const zeroRule: ResolvedRule | undefined = structure.grid?.zero && structure.grid.zero !== 'omit' - ? rule(structure.grid.zero, structureInk.rule ?? structureInk.axis, 'full') + ? rule(structure.grid.zero, structureInk.zero ?? structureInk.rule ?? structureInk.axis, 'full') : undefined; const frame = rule(structure.frame, structureInk.frame ?? structureInk.axis, 'omit'); diff --git a/packages/flint-js/src/core/theme/presets/economist.ts b/packages/flint-js/src/core/theme/presets/economist.ts index 51d1b95e..4f91e7f6 100644 --- a/packages/flint-js/src/core/theme/presets/economist.ts +++ b/packages/flint-js/src/core/theme/presets/economist.ts @@ -32,17 +32,18 @@ export const economist: ThemePreset = { "structure": { "grid": "#c9d3da", "axis": "#121317", - "rule": "#c9d3da" + "rule": "#c9d3da", + "zero": "#e3120b" }, "series": { "single": "#006ba2", "categorical": [ - "#3f5661", - "#a1655a", "#006ba2", - "#7ba7b8", "#3ebcd2", - "#c8b88a" + "#ebb434", + "#379a8b", + "#9a3d5b", + "#a17ba5" ], // Continuous measure: the Economist blue, light to deep. "sequential": { @@ -161,15 +162,16 @@ export const economist: ThemePreset = { }, "furniture": [ { - // The red rule runs about a tenth of the chart's width — the - // Economist style guide places it at ~1/8–1/10 of the graphic - // (≈46–58px on this ~460px plot). 46 keeps it a thin rule at - // the top-left, not a stub. + // The Economist "red tab" is a chunky rectangle, not a thin + // rule: the style guide draws it ~15pt wide × 5pt tall (≈3:1) + // on a 160pt chart — about 1/10 of the width. On this ~460px + // plot that is ≈44px wide; a ~12px height keeps the 3–4:1 block + // proportion so it reads as the masthead tag, not a hairline. "kind": "mastheadTab", "anchor": "topLeft", "color": "#e3120b", - "width": 46, - "height": 3 + "width": 44, + "height": 12 } ], "layout": { diff --git a/packages/flint-js/src/core/theme/types.ts b/packages/flint-js/src/core/theme/types.ts index 32eacb58..69c2660d 100644 --- a/packages/flint-js/src/core/theme/types.ts +++ b/packages/flint-js/src/core/theme/types.ts @@ -94,6 +94,14 @@ export interface ThemeInk { grid?: string; frame?: string; rule?: string; + /** + * The zero line, when the house makes it more than one gridline among + * the rest. The Economist strokes zero in its signature red so every + * length is read from a line the eye cannot miss. It borrows `rule` + * (then `axis`) where the house says nothing, so a house that draws an + * ordinary zero needs to state nothing. + */ + zero?: string; /** * The stem of a lollipop, the bridge of a dumbbell. It borrows `rule` * where the house says nothing, but the two are not the same job: a From 0a6fade72bbb7e58f31e2756c5f1e1f7c8144d61 Mon Sep 17 00:00:00 2001 From: Chenglong Wang Date: Sat, 1 Aug 2026 15:16:02 -0700 Subject: [PATCH 072/164] feat(theme): canvas-anchored masthead tab (graphic-left, gutter-independent) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Economist red tab is branding anchored to the graphic frame, not plot content. Modelled as a Vega-Lite concat child it pinned to the plot's data rectangle, so on a horizontal bar (wide left-axis gutter) it drifted right, away from the title, instead of holding graphic-left with it. No align/bounds combination lifts a concat child into the axis gutter — the wall is structural. Introduce "canvas furniture": the grounding stage records where the tab draws (graphic-left, flush with the title's own margin) in `usermeta`, which VL passes through compile untouched, and reserves a top band via padding so the tab opens the graphic above the headline. The renderer draws a plain onto the SVG at those absolute canvas coordinates — a real element inside the exported that travels with the file and rasterises identically in any standalone SVG renderer (verified with Resvg). Both the r2 harness and the site VegaLiteView inject it via shared helpers in canvas-furniture.ts. Because it draws after render, the tab now also appears on concats, radials and faceted tables (e.g. Economist pies), which the old concat-wrap approach skipped entirely. Verified flush-left tab/title alignment against the Economist 2017 style guide (page 6, "Chart dimensions"): tab and title share the left edge, tab directly above, ~15pt×5pt on a 160pt chart. Gates: flint tsc clean, site tsc clean, 45 theme + 697 full vitest, r2 0/85. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 33e4b998-4b58-4d7e-9cf8-c107b10e45c2 --- .../flint-js/src/vegalite/canvas-furniture.ts | 74 +++++++++++ packages/flint-js/src/vegalite/index.ts | 9 ++ packages/flint-js/src/vegalite/theme.ts | 120 ++++++++++++------ packages/flint-js/tests/theme-titles.test.ts | 42 +++--- scripts/theme-r2.ts | 4 +- site/src/components/VegaLiteView.tsx | 31 ++++- 6 files changed, 223 insertions(+), 57 deletions(-) create mode 100644 packages/flint-js/src/vegalite/canvas-furniture.ts diff --git a/packages/flint-js/src/vegalite/canvas-furniture.ts b/packages/flint-js/src/vegalite/canvas-furniture.ts new file mode 100644 index 00000000..676aeaea --- /dev/null +++ b/packages/flint-js/src/vegalite/canvas-furniture.ts @@ -0,0 +1,74 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** + * @module flint-chart/vegalite/canvas-furniture + * + * Canvas-anchored furniture: branding marks that belong to the *graphic frame*, + * not the plot. The Economist red masthead tab is the archetype — it sits at + * the graphic's top-left, flush with the title, and has nothing to do with the + * data rectangle. + * + * Vega-Lite has no way to express this. Every mark it draws lives in the plot's + * coordinate space, and a concat child pins to the plot's *data* rectangle — so + * on a horizontal bar (a wide left-axis gutter) the tab drifts right, away from + * the title, instead of holding graphic-left. There is no `align`/`bounds` + * combination that lifts a child into the axis gutter; the wall is structural. + * + * The remedy is to draw the tab *after* Vega-Lite is done, straight onto the + * rendered SVG at absolute canvas coordinates. The grounding stage records + * where each piece goes (in `usermeta`, which Vega-Lite passes through to the + * compiled Vega spec untouched) and reserves a top band so nothing overlaps the + * title. The renderer then injects a plain `` at those coordinates. The + * result is a real element inside the exported `` — it travels with the + * file and rasterises identically in any standalone SVG renderer. + */ + +/** A branding rectangle anchored to the graphic frame, in canvas pixels. */ +export interface CanvasFurnitureItem { + kind: string; + /** Distance from the canvas left edge — the same margin the title anchors to. */ + x: number; + /** Distance from the canvas top edge, inside the reserved top band. */ + y: number; + width: number; + height: number; + color: string; +} + +/** The `usermeta` key the grounding stage stamps canvas furniture under. */ +export const CANVAS_FURNITURE_KEY = 'flintCanvasFurniture'; + +/** + * Read canvas furniture off a spec's `usermeta`. Works on either a Vega-Lite + * spec (before compile) or the compiled Vega spec (`usermeta` survives compile). + */ +export function readCanvasFurniture(spec: any): CanvasFurnitureItem[] { + const items = spec?.usermeta?.[CANVAS_FURNITURE_KEY]; + return Array.isArray(items) ? items : []; +} + +function escapeAttr(value: string): string { + return String(value).replace(/&/g, '&').replace(/"/g, '"').replace(//g, '>'); +} + +/** Build the `` markup for a set of canvas-furniture items. */ +export function canvasFurnitureMarkup(items: CanvasFurnitureItem[]): string { + return items + .map( + (it) => + ``, + ) + .join(''); +} + +/** + * Inject canvas furniture into a rendered SVG string, drawn last so it paints + * over the background rect. A no-op when there is nothing to draw. + */ +export function injectCanvasFurnitureSVG(svg: string, items: CanvasFurnitureItem[]): string { + if (!items?.length) return svg; + const idx = svg.lastIndexOf(''); + if (idx === -1) return svg; + return svg.slice(0, idx) + canvasFurnitureMarkup(items) + svg.slice(idx); +} diff --git a/packages/flint-js/src/vegalite/index.ts b/packages/flint-js/src/vegalite/index.ts index c6a5d62e..e837543f 100644 --- a/packages/flint-js/src/vegalite/index.ts +++ b/packages/flint-js/src/vegalite/index.ts @@ -16,6 +16,15 @@ export { assembleVegaLite, getChartOptions, getChartPivot, getChartTransform } f // VL spec instantiation (Phase 2) export { vlApplyLayoutToSpec, vlApplyTooltips } from './instantiate-spec'; +// Canvas-anchored furniture (branding marks drawn onto the rendered SVG) +export { + type CanvasFurnitureItem, + CANVAS_FURNITURE_KEY, + readCanvasFurniture, + canvasFurnitureMarkup, + injectCanvasFurnitureSVG, +} from './canvas-furniture'; + // VL template registry export { vlTemplateDefs, diff --git a/packages/flint-js/src/vegalite/theme.ts b/packages/flint-js/src/vegalite/theme.ts index 565f9c80..bd82328a 100644 --- a/packages/flint-js/src/vegalite/theme.ts +++ b/packages/flint-js/src/vegalite/theme.ts @@ -18,6 +18,7 @@ import type { DesignDecisions, ThemeReport } from '../core/theme/types.js'; import { contrastingInk, parseColor, luminance, toHex } from '../core/theme/presence.js'; import { CONTINUOUS_BAR_STEP_FILL } from './templates/utils.js'; import { LOCAL_DODGE_LANE_FILL } from './templates/bar.js'; +import { CANVAS_FURNITURE_KEY, readCanvasFurniture, type CanvasFurnitureItem } from './canvas-furniture.js'; /** Mark families that carry data values (as opposed to chrome). */ const DATA_MARKS = new Set([ @@ -3906,44 +3907,54 @@ function facetedTableResistsFurniture(spec: any): boolean { function applyFurniture(spec: any, d: DesignDecisions, table: any[], say: (p: string, m: string) => void): boolean { if (!d.furniture.length) return false; + + // Two kinds of furniture with different homes. A masthead tab is *branding* + // anchored to the graphic frame: it draws onto the rendered canvas at + // graphic-left, flush with the title, and does not care what the plot is or + // how wide its axis gutter runs (see canvas-furniture.ts). A header/footer + // rule *closes a block*: it wraps the plot in a concatenation and runs the + // block's width. + const canvas = d.furniture.filter((f) => f.kind === 'mastheadTab'); + const rules = d.furniture.filter((f) => f.kind !== 'mastheadTab'); + + // Canvas furniture is drawn after render, so — unlike a block rule — it + // applies to every chart, including the concats, radials and faceted tables + // a rule cannot wrap. + let handled = false; + if (canvas.length) handled = applyCanvasFurniture(spec, canvas, d, say) || handled; + + if (!rules.length) return handled; + if (spec.vconcat || spec.hconcat || spec.concat) { - say('furniture', 'not drawn — the chart is already a concatenation'); - return false; + say('furniture', 'the closing rule is not drawn — the chart is already a concatenation'); + return handled; } // A rose or a faceted disc cannot be wrapped in a block to hang a rule // beneath it — see `radialResistsFurniture`. It is already its own block. if (radialResistsFurniture(spec)) { - say('furniture', 'not drawn — a radial chart is already its own block, with no edge to close'); - return false; + say('furniture', 'the closing rule is not drawn — a radial chart is already its own block, with no edge to close'); + return handled; } // A faceted table (facet of concat) collapses when wrapped — see // `facetedTableResistsFurniture`. It is already its own stack of blocks. if (facetedTableResistsFurniture(spec)) { - say('furniture', 'not drawn — a faceted table is already its own stack of blocks, with no single edge to close'); - return false; + say('furniture', 'the closing rule is not drawn — a faceted table is already its own stack of blocks, with no single edge to close'); + return handled; } - // A masthead tab opens the block *above* the headline — the Economist's - // red rule sits at the very top of the graphic, over the title, not - // between the title and the plot. A rule (header/footer) closes a block - // and stays where it is drawn: under the headline, or under the plot. - const aboveTitle: any[] = []; + // A rule closes a block, so its length is the block's, and a house that + // draws one does not state a number for it. Falling back to a stub leaves a + // dash in the corner that looks like a mistake rather than an edge. const before: any[] = []; const after: any[] = []; - // A tab is a mark of its own — the Economist's red rectangle is 26px - // because 26px is what it is. A rule is not: it closes the block, so its - // length is the block's, and a house that draws one does not state a - // number for it. Falling back to a stub leaves a dash in the corner that - // looks like a mistake rather than an edge. const block = blockWidth(spec, table); - for (const item of d.furniture) { - const isRule = item.kind !== 'mastheadTab'; - const width = item.width ?? (isRule ? block : 40); + for (const item of rules) { + const width = item.width ?? block; if (width == null) { say('furniture', `the house draws a ${item.kind} across the block, but the chart states no width to draw it across — left out`); continue; } - if (isRule && item.width == null) { + if (item.width == null) { say('furniture', `the ${item.kind} runs the width of the block — ${width}px — not a fixed stub`); } const isTop = (item.anchor ?? 'topLeft').startsWith('top'); @@ -3954,27 +3965,17 @@ function applyFurniture(spec: any, d: DesignDecisions, table: any[], say: (p: st height: item.height ?? 2, data: { values: [{}] }, }; - if (!isTop) after.push(rect); - else if (item.kind === 'mastheadTab') aboveTitle.push(rect); - else before.push(rect); + (isTop ? before : after).push(rect); } - if (!aboveTitle.length && !before.length && !after.length) return false; + if (!before.length && !after.length) return handled; const inner: any = { ...spec }; - for (const key of ['$schema', 'background', 'padding', 'title', 'config', 'autosize']) delete inner[key]; - - // The masthead tab must draw *over* the title, but the outer view's title - // always renders above every concatenated child. So when a tab opens the - // block, the title rides down onto the plot body and the tab is the first - // thing in the stack — tab, then headline, then chart. - const tabOverTitle = aboveTitle.length > 0 && !!spec.title; - if (tabOverTitle) inner.title = spec.title; + for (const key of ['$schema', 'background', 'padding', 'title', 'config', 'autosize', 'usermeta']) delete inner[key]; // A concatenated child's legends are hoisted to the outer view and drawn - // above every child in it — including the tab, which is the one thing that - // is supposed to open the block. Resolved independently they stay with the - // plot they key, and the house's rule sits back under the headline where it - // was drawn. + // above every child in it. Resolved independently they stay with the plot + // they key, and the house's rule sits back under the headline where it was + // drawn. const keyed = new Set(); walk(inner, (node) => { for (const channel of ['color', 'fill', 'stroke', 'size', 'shape', 'opacity'] as const) { @@ -3987,9 +3988,10 @@ function applyFurniture(spec: any, d: DesignDecisions, table: any[], say: (p: st ...(spec.$schema ? { $schema: spec.$schema } : {}), background: spec.background, padding: spec.padding, - ...(spec.title && !tabOverTitle ? { title: spec.title } : {}), + ...(spec.usermeta ? { usermeta: spec.usermeta } : {}), + ...(spec.title ? { title: spec.title } : {}), spacing: 6, - vconcat: [...aboveTitle, ...before, inner, ...after], + vconcat: [...before, inner, ...after], ...(keyed.size ? { resolve: { legend: Object.fromEntries([...keyed].map((c) => [c, 'independent'])) } } : {}), @@ -4000,6 +4002,48 @@ function applyFurniture(spec: any, d: DesignDecisions, table: any[], say: (p: st return true; } +/** Normalise Vega-Lite's number|object padding into a full-sided object. */ +function normalizePadding(p: any): { left: number; right: number; top: number; bottom: number } { + if (typeof p === 'number') return { left: p, right: p, top: p, bottom: p }; + return { left: 8, right: 8, top: 8, bottom: 8, ...(p ?? {}) }; +} + +/** + * Canvas-anchored branding (the Economist red tab). Records where each piece + * draws — graphic-left, flush with the title's own margin — and reserves a + * strip of top padding so the tab opens the graphic above the headline rather + * than colliding with it. The renderer draws the recorded rects onto the SVG; + * Vega-Lite carries the record through compile in `usermeta`. + */ +function applyCanvasFurniture( + spec: any, + items: DesignDecisions['furniture'], + d: DesignDecisions, + say: (p: string, m: string) => void, +): boolean { + const base = normalizePadding(spec.padding); + const marginLeft = base.left; // the same left margin the title anchors to + const topBefore = base.top; + const gapBelow = 8; + const built: CanvasFurnitureItem[] = []; + let band = 0; + for (const item of items) { + const width = item.width ?? 40; + const height = item.height ?? 2; + built.push({ kind: item.kind, x: marginLeft, y: topBefore, width, height, color: item.color ?? d.text.primary }); + band = Math.max(band, height); + } + if (!built.length) return false; + growPadding(spec, 'top', band + gapBelow); + const prev = readCanvasFurniture(spec); + spec.usermeta = { ...(spec.usermeta ?? {}), [CANVAS_FURNITURE_KEY]: [...prev, ...built] }; + say( + 'furniture', + `the masthead tab rides the canvas at graphic-left (${marginLeft}px), in a ${band}px band above the title — clear of the plot's axis gutter`, + ); + return true; +} + function clamp(v: number, lo: number, hi: number): number { return Math.max(lo, Math.min(hi, v)); } diff --git a/packages/flint-js/tests/theme-titles.test.ts b/packages/flint-js/tests/theme-titles.test.ts index 6d74c0fd..0c1d9842 100644 --- a/packages/flint-js/tests/theme-titles.test.ts +++ b/packages/flint-js/tests/theme-titles.test.ts @@ -109,11 +109,14 @@ describe('legend titles', () => { }); /** - * A masthead tab opens the block above the headline. + * A masthead tab is canvas furniture — branding anchored to the graphic frame. * - * The Economist's red rule sits at the very top of the graphic, over the - * title — not between the title and the plot. So a house that draws one keeps - * the tab first in the stack and lets the headline ride down onto the plot. + * The Economist's red tab sits at the very top-left of the graphic, flush with + * the title's own margin, and has nothing to do with the plot. So it is not a + * concat child (which would pin to the plot's data rectangle and drift right on + * a wide axis gutter) — it is recorded in `usermeta` for the renderer to draw + * onto the SVG, and a strip of top padding is reserved so it opens the graphic + * above the headline. */ describe('masthead furniture', () => { function withTab(): any { @@ -136,17 +139,26 @@ describe('masthead furniture', () => { } as any) as any; } - it('draws the tab first and above the title', () => { + it('records the tab as canvas furniture, flush with the title, and keeps the title on the graphic', () => { const spec = withTab(); - expect(Array.isArray(spec.vconcat)).toBe(true); - // The tab opens the stack. - const first = spec.vconcat[0]; - expect(first.__themeSynthetic).toBe(true); - expect(first.mark.type).toBe('rect'); - expect(first.mark.color).toBe('#e3120b'); - // The title has come off the outer view and ridden down onto the plot. - expect(spec.title).toBeUndefined(); - const titled = spec.vconcat.find((v: any) => v && v.title); - expect(titled).toBeTruthy(); + // The tab is not wrapped into a concat — it draws onto the canvas. + expect(spec.vconcat).toBeUndefined(); + const tabs = spec.usermeta?.flintCanvasFurniture; + expect(Array.isArray(tabs)).toBe(true); + expect(tabs).toHaveLength(1); + const tab = tabs[0]; + expect(tab.kind).toBe('mastheadTab'); + expect(tab.color).toBe('#e3120b'); + expect(tab.width).toBe(26); + expect(tab.height).toBe(3); + // The tab anchors to the same left margin the title anchors to. + const pad = spec.padding; + const left = typeof pad === 'number' ? pad : pad.left; + expect(tab.x).toBe(left); + // A band of top padding is reserved so the tab opens above the headline. + const top = typeof pad === 'number' ? pad : pad.top; + expect(top).toBeGreaterThanOrEqual(tab.y + tab.height); + // The headline stays on the graphic frame. + expect(spec.title).toBeTruthy(); }); }); diff --git a/scripts/theme-r2.ts b/scripts/theme-r2.ts index 380ded1f..fb6396ca 100644 --- a/scripts/theme-r2.ts +++ b/scripts/theme-r2.ts @@ -33,6 +33,7 @@ import * as vega from 'vega'; import { Resvg } from '@resvg/resvg-js'; import { assembleVegaLite } from '../packages/flint-js/src/index'; +import { injectCanvasFurnitureSVG, readCanvasFurniture } from '../packages/flint-js/src/vegalite/canvas-furniture'; import { THEME_PRESETS } from '../packages/flint-js/src/core/theme/presets'; import { R2_CASES, r2Input, type R2Case } from '../site/src/playground/theme-lab-r2-data'; @@ -64,8 +65,9 @@ interface Panel { svg: string; width: number; height: number; label: string; bac async function toSvg(spec: any): Promise<{ svg: string; width: number; height: number }> { const vgSpec = compile(spec).spec; const view = new vega.View(vega.parse(vgSpec), { renderer: 'none' }); - const svg = await view.toSVG(); + let svg = await view.toSVG(); view.finalize(); + svg = injectCanvasFurnitureSVG(svg, readCanvasFurniture(vgSpec)); const m = /]*\bwidth="([\d.]+)"[^>]*\bheight="([\d.]+)"/.exec(svg); return { svg, width: m ? Number(m[1]) : 400, height: m ? Number(m[2]) : 300 }; } diff --git a/site/src/components/VegaLiteView.tsx b/site/src/components/VegaLiteView.tsx index 366aeadc..e79b964c 100644 --- a/site/src/components/VegaLiteView.tsx +++ b/site/src/components/VegaLiteView.tsx @@ -1,14 +1,39 @@ import { useEffect, useRef } from 'react'; import embed from 'vega-embed'; +import { readCanvasFurniture } from 'flint-chart'; + +const SVG_NS = 'http://www.w3.org/2000/svg'; export function VegaLiteView({ spec, renderer = 'canvas' }: { spec: any; renderer?: 'canvas' | 'svg' }) { const ref = useRef(null); useEffect(() => { if (!ref.current) return; + const host = ref.current; let cancelled = false; - embed(ref.current, spec, { actions: false, renderer }).catch((err) => { - if (!cancelled) console.error('vega-embed failed', err); - }); + // Canvas-anchored furniture (the Economist red tab) is drawn onto the SVG + // after render — Vega-Lite cannot express it. That requires SVG output, so + // a spec carrying furniture is forced to render as SVG regardless of the + // requested renderer. + const furniture = readCanvasFurniture(spec); + const useRenderer = furniture.length ? 'svg' : renderer; + embed(host, spec, { actions: false, renderer: useRenderer }) + .then(() => { + if (cancelled || !furniture.length) return; + const svgEl = host.querySelector('svg'); + if (!svgEl) return; + for (const it of furniture) { + const rect = document.createElementNS(SVG_NS, 'rect'); + rect.setAttribute('x', String(it.x)); + rect.setAttribute('y', String(it.y)); + rect.setAttribute('width', String(it.width)); + rect.setAttribute('height', String(it.height)); + rect.setAttribute('fill', it.color); + svgEl.appendChild(rect); + } + }) + .catch((err) => { + if (!cancelled) console.error('vega-embed failed', err); + }); return () => { cancelled = true; }; From 8e6bf00a19c13e17a810b155faf2d6edeca1b95c Mon Sep 17 00:00:00 2001 From: Chenglong Wang Date: Sat, 1 Aug 2026 23:34:38 -0700 Subject: [PATCH 073/164] Fix Power BI dark-theme structural-mark visibility MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The dark Power BI house left three structural marks unreadable on its near-black canvas, each falling back to an ink chosen against white: - Lollipop stems and dumbbell/waterfall connectors had no house ink, so Vega-Lite defaulted them to black — invisible on #1b1a19. Add a structure.connector ink and a marks.connector so applyConnectors paints them a visible neutral grey; mirror the ink on powerbi-light for parity (removing its reliance on a black default). - The waterfall 'increase' role took the diverging neutral (#4a4948), a dark grey that sank into the canvas. A diverging/sequential ramp sampled onto a *discrete* colour domain now re-tones its near-neutral entries to the same distance from the surface (as literal marks already do), while continuous heat ramps are left untouched. - Gridlines were dashed; real Power BI (and powerbi-light) uses solid faint gridlines. Switch the dark house to solid so the quiet grid reads. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 33e4b998-4b58-4d7e-9cf8-c107b10e45c2 --- .../src/core/theme/presets/powerbi-light.ts | 8 +++++++- packages/flint-js/src/core/theme/presets/powerbi.ts | 10 ++++++++-- packages/flint-js/src/vegalite/theme.ts | 13 ++++++++++++- 3 files changed, 27 insertions(+), 4 deletions(-) diff --git a/packages/flint-js/src/core/theme/presets/powerbi-light.ts b/packages/flint-js/src/core/theme/presets/powerbi-light.ts index e5534ad6..2f3bd8a1 100644 --- a/packages/flint-js/src/core/theme/presets/powerbi-light.ts +++ b/packages/flint-js/src/core/theme/presets/powerbi-light.ts @@ -39,7 +39,8 @@ export const powerbiLight: ThemePreset = { "structure": { "grid": "#ededed", "axis": "#d2d0ce", - "rule": "#d2d0ce" + "rule": "#d2d0ce", + "connector": "#8a8886" }, "series": { "single": "#118dff", @@ -162,6 +163,11 @@ export const powerbiLight: ThemePreset = { "slice": { "gap": 1.5 }, + "connector": { + "presence": "full", + "weight": 1.5, + "spanWeight": 2 + }, "trailingFill": { "presence": "quiet", "opacity": 0.18 diff --git a/packages/flint-js/src/core/theme/presets/powerbi.ts b/packages/flint-js/src/core/theme/presets/powerbi.ts index f82c270f..94353b09 100644 --- a/packages/flint-js/src/core/theme/presets/powerbi.ts +++ b/packages/flint-js/src/core/theme/presets/powerbi.ts @@ -35,7 +35,8 @@ export const powerbi: ThemePreset = { "structure": { "grid": "#3b3a39", "axis": "#3b3a39", - "rule": "#3b3a39" + "rule": "#3b3a39", + "connector": "#797775" }, "series": { "single": "#118dff", @@ -144,7 +145,7 @@ export const powerbi: ThemePreset = { "grid": { "measure": "quiet", "category": "omit", - "style": "dashed" + "style": "solid" }, "frame": "omit", "baseline": "quiet" @@ -161,6 +162,11 @@ export const powerbi: ThemePreset = { "slice": { "gap": 1.5 }, + "connector": { + "presence": "full", + "weight": 1.5, + "spanWeight": 2 + }, "trailingFill": { "presence": "quiet", "opacity": 0.18 diff --git a/packages/flint-js/src/vegalite/theme.ts b/packages/flint-js/src/vegalite/theme.ts index bd82328a..ea44bfeb 100644 --- a/packages/flint-js/src/vegalite/theme.ts +++ b/packages/flint-js/src/vegalite/theme.ts @@ -1878,7 +1878,18 @@ function applySeriesInk(spec: any, d: DesignDecisions, table: any[], say: (p: st } if ((s.mode === 'sequential' || s.mode === 'diverging') && s.range?.length) { const quantize = s.quantize && enc.type === 'quantitative'; - setColorRange(enc, s.range, quantize ? { type: 'quantize' } : undefined); + // A diverging/sequential ramp sampled onto a *discrete* colour + // domain (a waterfall's total/increase/decrease, say) can hand + // a near-neutral midpoint to a bar fill. That neutral was + // placed against a light surface; on a dark canvas it sinks + // into the background. Re-tone the neutral entries to the same + // distance from this surface, leaving the hued endpoints — and + // every continuous heat ramp — untouched. + const surface = d.surface.plot ?? d.surface.canvas; + const range = isContinuousColor(enc) + ? s.range + : s.range.map((c) => reToneNeutral(c, surface)); + setColorRange(enc, range, quantize ? { type: 'quantize' } : undefined); continue; } // Categorical: an ordered set consumed by index, with the overflow From d96f923dfe3a6fa17af38e46e9f776c0966a0467 Mon Sep 17 00:00:00 2001 From: Chenglong Wang Date: Sun, 2 Aug 2026 00:02:21 -0700 Subject: [PATCH 074/164] Add playful Cartoon theme + rounded-bar mark lever MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A fresh, fun house in the spirit of xkcd and flat-cartoon illustration: warm sketch-paper canvas, a rounded comic typeface (Comic Sans / Comic Neue / Chalkboard fallbacks), fat round-capped round-joined strokes, bouncy monotone curves, chunky dots, and a bright crayon palette. Flint can't draw the hand-wobbled 'last mile' of a true xkcd plot (a per-pixel filter, not a chart decision), so the character is carried by the parts a theme owns — font, axes, colour, and mark shape. To round the bars (a strong cartoon signal, and a legitimate house property), add a generalizable marks.cornerRadius lever: it maps to Vega-Lite's cornerRadiusEnd so only the value end is rounded — bar tops, horizontal-bar right ends, and the outer end of a stack — while baselines stay crisp and stacks still read as one column. Houses that say nothing keep square corners. Wired schema (ThemeMarks + ResolvedMarks) -> grounding -> realization; the preset registers in THEME_PRESETS, so it appears automatically as a column in the r2 theme lab. Verified across bar / stacked / horizontal / line / pie / scatter. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 33e4b998-4b58-4d7e-9cf8-c107b10e45c2 --- packages/flint-js/src/core/theme/ground.ts | 1 + packages/flint-js/src/core/theme/presets.ts | 2 + .../src/core/theme/presets/cartoon.ts | 204 ++++++++++++++++++ packages/flint-js/src/core/theme/types.ts | 10 + packages/flint-js/src/vegalite/theme.ts | 6 + 5 files changed, 223 insertions(+) create mode 100644 packages/flint-js/src/core/theme/presets/cartoon.ts diff --git a/packages/flint-js/src/core/theme/ground.ts b/packages/flint-js/src/core/theme/ground.ts index da23618c..9ab292fe 100644 --- a/packages/flint-js/src/core/theme/ground.ts +++ b/packages/flint-js/src/core/theme/ground.ts @@ -1156,6 +1156,7 @@ export function groundTheme(themeIn: ThemeSpec, ctx: GroundingContext): DesignDe ? 'monotone' : marksSpec.interpolation === 'step' ? 'step' : undefined, fillOpacity: marksSpec.fillOpacity, + cornerRadius: marksSpec.cornerRadius, point: marksSpec.point || halo ? { show: (marksSpec.point?.presence ?? 'omit') !== 'omit', diff --git a/packages/flint-js/src/core/theme/presets.ts b/packages/flint-js/src/core/theme/presets.ts index b9a5ae25..b38d8641 100644 --- a/packages/flint-js/src/core/theme/presets.ts +++ b/packages/flint-js/src/core/theme/presets.ts @@ -18,6 +18,7 @@ import { datawrapper } from './presets/datawrapper'; import { powerbi } from './presets/powerbi'; import { powerbiLight } from './presets/powerbi-light'; import { swiss } from './presets/swiss'; +import { cartoon } from './presets/cartoon'; export const THEME_PRESETS: Record = { nyt, @@ -28,6 +29,7 @@ export const THEME_PRESETS: Record = { powerbi, 'powerbi-light': powerbiLight, swiss, + cartoon, }; /** The catalogue, without the specs — enough to choose by. */ diff --git a/packages/flint-js/src/core/theme/presets/cartoon.ts b/packages/flint-js/src/core/theme/presets/cartoon.ts new file mode 100644 index 00000000..c15929e7 --- /dev/null +++ b/packages/flint-js/src/core/theme/presets/cartoon.ts @@ -0,0 +1,204 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import type { ThemePreset } from '../types'; + +/** + * Cartoon. + * + * A playful, colourful house in the spirit of xkcd and modern flat-cartoon + * illustration. Flint cannot draw the hand-wobbled "last mile" of a true xkcd + * plot — the jitter is a per-pixel filter, not a chart decision — so the fun + * is carried by the parts a theme *does* own: a rounded comic typeface, fat + * round-capped strokes, rounded bar tops, bouncy monotone curves, and a bright + * crayon palette on warm sketch-paper. The result reads as friendly and + * approachable rather than clinical — a fresh, engaging variant beside the + * editorial and dashboard houses. + */ +export const cartoon: ThemePreset = { + id: 'cartoon', + label: 'Cartoon', + description: + 'Playful flat-cartoon look: warm sketch-paper, a rounded comic typeface, fat round-capped strokes, rounded bar tops and bouncy curves, in a bright crayon palette.', + guidance: [ + '- `title` sets the scene in the big comic hand; `subtitle` names the measure.', + '- Keep it light — a cartoon chart is for one clear point, not a dense table.', + '- Colour tells 6 categories apart with a bright crayon set.', + ].join('\n'), + spec: { + id: 'cartoon', + label: 'Cartoon', + ink: { + surface: { + source: 'house', + canvas: '#fffdf5', + plot: '#fffdf5', + }, + text: { + primary: '#3a352f', + secondary: '#6b655c', + muted: '#9a9488', + }, + structure: { + grid: '#e9e3d6', + axis: '#3a352f', + rule: '#3a352f', + connector: '#9a9488', + }, + series: { + single: '#ff5d5d', + categorical: ['#2f9ee6', '#ff5d5d', '#ffc23c', '#4cc76a', '#9b6cff', '#ff8a3d'], + categoricalExtended: [ + '#2f9ee6', + '#ff5d5d', + '#ffc23c', + '#4cc76a', + '#9b6cff', + '#ff8a3d', + '#26c6b8', + '#ff6fb5', + '#a8d84a', + '#5b6cff', + '#b5794e', + '#d94fc9', + ], + // A single-hue sky-blue ramp, binned so a reader can name a bin + // off the key rather than squint at a wash. + sequential: { + stops: ['#e4f3fc', '#a7d8f5', '#5cb7ee', '#2f9ee6', '#1668a8'], + space: 'lab', + endpointsAgainstSurface: true, + consumption: 'quantize', + quantizeCount: 5, + }, + // Coral to sky-blue through the warm paper neutral. + diverging: { + stops: ['#ff5d5d', '#ffb0a3', '#efe9dc', '#9ecbef', '#2f9ee6'], + neutral: '#efe9dc', + space: 'lab', + endpointsAgainstSurface: true, + consumption: 'quantize', + quantizeCount: 5, + }, + // Signed data reads as thumbs-up green and uh-oh coral, with a + // soft grey for the anchoring total. + status: { + positive: '#4cc76a', + negative: '#ff5d5d', + neutral: '#b7b2a6', + }, + overflow: '#b7b2a6', + selection: { + signed: 'status', + statusUse: 'anySigned', + }, + }, + accent: '#ff5d5d', + }, + type: { + minSize: 9, + headline: { + family: "'Comic Sans MS', 'Comic Neue', 'Chalkboard SE', 'Marker Felt', cursive", + size: 'text.400', + weight: 'bold', + color: '#3a352f', + }, + deck: { + family: "'Comic Sans MS', 'Comic Neue', 'Chalkboard SE', 'Marker Felt', cursive", + size: 'text.200', + color: '#6b655c', + }, + axisLabel: { + family: "'Comic Sans MS', 'Comic Neue', 'Chalkboard SE', 'Marker Felt', cursive", + size: 'text.100', + }, + axisTitle: { + family: "'Comic Sans MS', 'Comic Neue', 'Chalkboard SE', 'Marker Felt', cursive", + size: 'text.100', + weight: 'bold', + color: '#3a352f', + }, + keyLabel: { + family: "'Comic Sans MS', 'Comic Neue', 'Chalkboard SE', 'Marker Felt', cursive", + size: 'text.100', + }, + }, + structure: { + axis: { + categorical: { + line: 'full', + ticks: 'omit', + }, + measure: { + line: 'full', + ticks: 'omit', + tickLabels: 'sparse', + }, + }, + grid: { + measure: 'quiet', + category: 'omit', + style: 'dashed', + zero: 'full', + }, + frame: 'omit', + baseline: 'full', + }, + marks: { + bandFraction: 0.66, + strokeWeight: 3.5, + strokeCap: 'round', + strokeJoin: 'round', + interpolation: 'monotone', + cornerRadius: 6, + point: { + presence: 'full', + fill: 'solid', + size: 60, + }, + separator: { + presence: 'hairline', + source: 'surface', + width: 2, + }, + slice: { + gap: 3, + gapStyle: 'rule', + }, + sizeRange: [24, 500], + }, + labels: { + truncation: 'never', + angle: 'auto', + }, + legend: { + show: 'always', + placement: ['top', 'right'], + direction: 'horizontal', + title: 'whenAmbiguous', + suppressWhenAxisNames: true, + }, + dataLabels: { + show: 'whenTheyFit', + placement: 'outsideMark', + inkMode: 'fixed', + }, + annotation: { + axisTitles: 'whenAmbiguous', + unit: 'lastTick', + numberFormat: { + precision: 'auto', + }, + }, + layout: { + density: 'normal', + titleBlock: { + anchor: 'start', + gap: 'normal', + }, + }, + compileDefaults: { + baseSize: { width: 420, height: 320 }, + }, + }, +}; diff --git a/packages/flint-js/src/core/theme/types.ts b/packages/flint-js/src/core/theme/types.ts index 69c2660d..ddf8cdf2 100644 --- a/packages/flint-js/src/core/theme/types.ts +++ b/packages/flint-js/src/core/theme/types.ts @@ -194,6 +194,14 @@ export interface ThemeMarks { strokeJoin?: 'miter' | 'round' | 'bevel'; interpolation?: 'linear' | 'monotone' | 'step'; fillOpacity?: number; + /** + * How far the *value* end of a bar is rounded, in px — the top of a + * column, the right of a horizontal bar. Only the end moves; the baseline + * stays a clean edge, so a stack still reads as one column. A house that + * says nothing keeps square corners; a friendlier, less clinical house + * rounds them. + */ + cornerRadius?: number; sizeRange?: [number, number]; minSize?: number; zOrder?: 'summaryOverData' | 'summaryUnderData'; @@ -625,6 +633,8 @@ export interface ResolvedMarks { strokeJoin?: string; interpolate?: string; fillOpacity?: number; + /** Corner radius for the value end of a bar, in px. */ + cornerRadius?: number; point?: { show: boolean; size?: number; filled?: boolean; haloColor?: string; haloWidth?: number }; /** The area a sized mark may take, smallest to largest, in px². */ sizeRange?: [number, number]; diff --git a/packages/flint-js/src/vegalite/theme.ts b/packages/flint-js/src/vegalite/theme.ts index ea44bfeb..9e2e3a82 100644 --- a/packages/flint-js/src/vegalite/theme.ts +++ b/packages/flint-js/src/vegalite/theme.ts @@ -928,6 +928,12 @@ function applyMarks(spec: any, d: DesignDecisions, table: any[], say: (p: string if (m.strokeJoin) config.line.strokeJoin = m.strokeJoin; if (m.interpolate) config.line.interpolate = m.interpolate; if (m.fillOpacity != null) config.area = { ...(config.area ?? {}), fillOpacity: m.fillOpacity }; + if (m.cornerRadius != null) { + // Round only the value end so the baseline stays a clean edge and a + // stack still reads as one column. `cornerRadiusEnd` rounds the top of + // a vertical bar and the right of a horizontal one. + config.bar = { ...(config.bar ?? {}), cornerRadiusEnd: m.cornerRadius }; + } protectDashEncoding(spec, config, m.strokeWidth); if (m.point?.show) { From fbc21ff6ba7c2ee319f76bab4a62f9d20f173d42 Mon Sep 17 00:00:00 2001 From: Chenglong Wang Date: Sun, 2 Aug 2026 00:37:24 -0700 Subject: [PATCH 075/164] Revert Cartoon theme; add Cartoon lab + group theme-lab nav MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The pipeline Cartoon preset felt flat, so pull it out and explore the look by hand first, the way the Swiss lab did before the swiss preset landed. - Revert the 'cartoon' preset and the marks.cornerRadius lever it added (flint-js is back to exactly the pre-cartoon state); the handmade lab uses raw Vega-Lite, so no theme lever is needed yet. - Add a Cartoon lab (site/src/playground/CartoonLab.tsx + cartoon-lab-data.ts): six hand-authored specs — sticker bars (rounded tops + fat dark outline), a bouncy haloed line, bubble buddies, a gumball pie, emoji lollipops, and layer-cake stacks — on warm paper in a bright crayon palette. These probe which levers actually read as fun, and which (emoji markers) the theme spec would need to grow. - Tidy the dev nav: collapse the six theme-lab pages (Theme lab, R2, real, gaps, Swiss lab, Cartoon lab) under a single 'Theme labs' hover/focus dropdown in PlaygroundShell, with matching CSS. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 33e4b998-4b58-4d7e-9cf8-c107b10e45c2 --- packages/flint-js/src/core/theme/ground.ts | 1 - packages/flint-js/src/core/theme/presets.ts | 2 - .../src/core/theme/presets/cartoon.ts | 204 --------- packages/flint-js/src/core/theme/types.ts | 10 - packages/flint-js/src/vegalite/theme.ts | 6 - site/src/main.tsx | 2 + site/src/playground/CartoonLab.tsx | 75 ++++ site/src/playground/PlaygroundShell.tsx | 69 ++- site/src/playground/cartoon-lab-data.ts | 397 ++++++++++++++++++ site/src/playground/playground.css | 47 +++ 10 files changed, 576 insertions(+), 237 deletions(-) delete mode 100644 packages/flint-js/src/core/theme/presets/cartoon.ts create mode 100644 site/src/playground/CartoonLab.tsx create mode 100644 site/src/playground/cartoon-lab-data.ts diff --git a/packages/flint-js/src/core/theme/ground.ts b/packages/flint-js/src/core/theme/ground.ts index 9ab292fe..da23618c 100644 --- a/packages/flint-js/src/core/theme/ground.ts +++ b/packages/flint-js/src/core/theme/ground.ts @@ -1156,7 +1156,6 @@ export function groundTheme(themeIn: ThemeSpec, ctx: GroundingContext): DesignDe ? 'monotone' : marksSpec.interpolation === 'step' ? 'step' : undefined, fillOpacity: marksSpec.fillOpacity, - cornerRadius: marksSpec.cornerRadius, point: marksSpec.point || halo ? { show: (marksSpec.point?.presence ?? 'omit') !== 'omit', diff --git a/packages/flint-js/src/core/theme/presets.ts b/packages/flint-js/src/core/theme/presets.ts index b38d8641..b9a5ae25 100644 --- a/packages/flint-js/src/core/theme/presets.ts +++ b/packages/flint-js/src/core/theme/presets.ts @@ -18,7 +18,6 @@ import { datawrapper } from './presets/datawrapper'; import { powerbi } from './presets/powerbi'; import { powerbiLight } from './presets/powerbi-light'; import { swiss } from './presets/swiss'; -import { cartoon } from './presets/cartoon'; export const THEME_PRESETS: Record = { nyt, @@ -29,7 +28,6 @@ export const THEME_PRESETS: Record = { powerbi, 'powerbi-light': powerbiLight, swiss, - cartoon, }; /** The catalogue, without the specs — enough to choose by. */ diff --git a/packages/flint-js/src/core/theme/presets/cartoon.ts b/packages/flint-js/src/core/theme/presets/cartoon.ts deleted file mode 100644 index c15929e7..00000000 --- a/packages/flint-js/src/core/theme/presets/cartoon.ts +++ /dev/null @@ -1,204 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -import type { ThemePreset } from '../types'; - -/** - * Cartoon. - * - * A playful, colourful house in the spirit of xkcd and modern flat-cartoon - * illustration. Flint cannot draw the hand-wobbled "last mile" of a true xkcd - * plot — the jitter is a per-pixel filter, not a chart decision — so the fun - * is carried by the parts a theme *does* own: a rounded comic typeface, fat - * round-capped strokes, rounded bar tops, bouncy monotone curves, and a bright - * crayon palette on warm sketch-paper. The result reads as friendly and - * approachable rather than clinical — a fresh, engaging variant beside the - * editorial and dashboard houses. - */ -export const cartoon: ThemePreset = { - id: 'cartoon', - label: 'Cartoon', - description: - 'Playful flat-cartoon look: warm sketch-paper, a rounded comic typeface, fat round-capped strokes, rounded bar tops and bouncy curves, in a bright crayon palette.', - guidance: [ - '- `title` sets the scene in the big comic hand; `subtitle` names the measure.', - '- Keep it light — a cartoon chart is for one clear point, not a dense table.', - '- Colour tells 6 categories apart with a bright crayon set.', - ].join('\n'), - spec: { - id: 'cartoon', - label: 'Cartoon', - ink: { - surface: { - source: 'house', - canvas: '#fffdf5', - plot: '#fffdf5', - }, - text: { - primary: '#3a352f', - secondary: '#6b655c', - muted: '#9a9488', - }, - structure: { - grid: '#e9e3d6', - axis: '#3a352f', - rule: '#3a352f', - connector: '#9a9488', - }, - series: { - single: '#ff5d5d', - categorical: ['#2f9ee6', '#ff5d5d', '#ffc23c', '#4cc76a', '#9b6cff', '#ff8a3d'], - categoricalExtended: [ - '#2f9ee6', - '#ff5d5d', - '#ffc23c', - '#4cc76a', - '#9b6cff', - '#ff8a3d', - '#26c6b8', - '#ff6fb5', - '#a8d84a', - '#5b6cff', - '#b5794e', - '#d94fc9', - ], - // A single-hue sky-blue ramp, binned so a reader can name a bin - // off the key rather than squint at a wash. - sequential: { - stops: ['#e4f3fc', '#a7d8f5', '#5cb7ee', '#2f9ee6', '#1668a8'], - space: 'lab', - endpointsAgainstSurface: true, - consumption: 'quantize', - quantizeCount: 5, - }, - // Coral to sky-blue through the warm paper neutral. - diverging: { - stops: ['#ff5d5d', '#ffb0a3', '#efe9dc', '#9ecbef', '#2f9ee6'], - neutral: '#efe9dc', - space: 'lab', - endpointsAgainstSurface: true, - consumption: 'quantize', - quantizeCount: 5, - }, - // Signed data reads as thumbs-up green and uh-oh coral, with a - // soft grey for the anchoring total. - status: { - positive: '#4cc76a', - negative: '#ff5d5d', - neutral: '#b7b2a6', - }, - overflow: '#b7b2a6', - selection: { - signed: 'status', - statusUse: 'anySigned', - }, - }, - accent: '#ff5d5d', - }, - type: { - minSize: 9, - headline: { - family: "'Comic Sans MS', 'Comic Neue', 'Chalkboard SE', 'Marker Felt', cursive", - size: 'text.400', - weight: 'bold', - color: '#3a352f', - }, - deck: { - family: "'Comic Sans MS', 'Comic Neue', 'Chalkboard SE', 'Marker Felt', cursive", - size: 'text.200', - color: '#6b655c', - }, - axisLabel: { - family: "'Comic Sans MS', 'Comic Neue', 'Chalkboard SE', 'Marker Felt', cursive", - size: 'text.100', - }, - axisTitle: { - family: "'Comic Sans MS', 'Comic Neue', 'Chalkboard SE', 'Marker Felt', cursive", - size: 'text.100', - weight: 'bold', - color: '#3a352f', - }, - keyLabel: { - family: "'Comic Sans MS', 'Comic Neue', 'Chalkboard SE', 'Marker Felt', cursive", - size: 'text.100', - }, - }, - structure: { - axis: { - categorical: { - line: 'full', - ticks: 'omit', - }, - measure: { - line: 'full', - ticks: 'omit', - tickLabels: 'sparse', - }, - }, - grid: { - measure: 'quiet', - category: 'omit', - style: 'dashed', - zero: 'full', - }, - frame: 'omit', - baseline: 'full', - }, - marks: { - bandFraction: 0.66, - strokeWeight: 3.5, - strokeCap: 'round', - strokeJoin: 'round', - interpolation: 'monotone', - cornerRadius: 6, - point: { - presence: 'full', - fill: 'solid', - size: 60, - }, - separator: { - presence: 'hairline', - source: 'surface', - width: 2, - }, - slice: { - gap: 3, - gapStyle: 'rule', - }, - sizeRange: [24, 500], - }, - labels: { - truncation: 'never', - angle: 'auto', - }, - legend: { - show: 'always', - placement: ['top', 'right'], - direction: 'horizontal', - title: 'whenAmbiguous', - suppressWhenAxisNames: true, - }, - dataLabels: { - show: 'whenTheyFit', - placement: 'outsideMark', - inkMode: 'fixed', - }, - annotation: { - axisTitles: 'whenAmbiguous', - unit: 'lastTick', - numberFormat: { - precision: 'auto', - }, - }, - layout: { - density: 'normal', - titleBlock: { - anchor: 'start', - gap: 'normal', - }, - }, - compileDefaults: { - baseSize: { width: 420, height: 320 }, - }, - }, -}; diff --git a/packages/flint-js/src/core/theme/types.ts b/packages/flint-js/src/core/theme/types.ts index ddf8cdf2..69c2660d 100644 --- a/packages/flint-js/src/core/theme/types.ts +++ b/packages/flint-js/src/core/theme/types.ts @@ -194,14 +194,6 @@ export interface ThemeMarks { strokeJoin?: 'miter' | 'round' | 'bevel'; interpolation?: 'linear' | 'monotone' | 'step'; fillOpacity?: number; - /** - * How far the *value* end of a bar is rounded, in px — the top of a - * column, the right of a horizontal bar. Only the end moves; the baseline - * stays a clean edge, so a stack still reads as one column. A house that - * says nothing keeps square corners; a friendlier, less clinical house - * rounds them. - */ - cornerRadius?: number; sizeRange?: [number, number]; minSize?: number; zOrder?: 'summaryOverData' | 'summaryUnderData'; @@ -633,8 +625,6 @@ export interface ResolvedMarks { strokeJoin?: string; interpolate?: string; fillOpacity?: number; - /** Corner radius for the value end of a bar, in px. */ - cornerRadius?: number; point?: { show: boolean; size?: number; filled?: boolean; haloColor?: string; haloWidth?: number }; /** The area a sized mark may take, smallest to largest, in px². */ sizeRange?: [number, number]; diff --git a/packages/flint-js/src/vegalite/theme.ts b/packages/flint-js/src/vegalite/theme.ts index 9e2e3a82..ea44bfeb 100644 --- a/packages/flint-js/src/vegalite/theme.ts +++ b/packages/flint-js/src/vegalite/theme.ts @@ -928,12 +928,6 @@ function applyMarks(spec: any, d: DesignDecisions, table: any[], say: (p: string if (m.strokeJoin) config.line.strokeJoin = m.strokeJoin; if (m.interpolate) config.line.interpolate = m.interpolate; if (m.fillOpacity != null) config.area = { ...(config.area ?? {}), fillOpacity: m.fillOpacity }; - if (m.cornerRadius != null) { - // Round only the value end so the baseline stays a clean edge and a - // stack still reads as one column. `cornerRadiusEnd` rounds the top of - // a vertical bar and the right of a horizontal one. - config.bar = { ...(config.bar ?? {}), cornerRadiusEnd: m.cornerRadius }; - } protectDashEncoding(spec, config, m.strokeWidth); if (m.point?.show) { diff --git a/site/src/main.tsx b/site/src/main.tsx index 88e7130e..a536ce2a 100644 --- a/site/src/main.tsx +++ b/site/src/main.tsx @@ -20,6 +20,7 @@ import { ThemeLabR2 } from './playground/ThemeLabR2'; import { ThemeLabReal } from './playground/ThemeLabReal'; import { ThemeLabGaps } from './playground/ThemeLabGaps'; import { SwissLab } from './playground/SwissLab'; +import { CartoonLab } from './playground/CartoonLab'; import { FullTestCases } from './playground/FullTestCases'; import { LocaleProvider, useLocale } from './i18n/LocaleContext'; import type { Locale } from './i18n/locales'; @@ -62,6 +63,7 @@ function AppRoutes({ locale }: { locale: Locale }) { } /> } /> } /> + } /> } />
{/* Tutorials merged into Documentation as the "Quick start" group. */} diff --git a/site/src/playground/CartoonLab.tsx b/site/src/playground/CartoonLab.tsx new file mode 100644 index 00000000..73ccf4f1 --- /dev/null +++ b/site/src/playground/CartoonLab.tsx @@ -0,0 +1,75 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** + * Cartoon lab — hand-authored Vega-Lite mockups of a playful "cartoon" look, + * laid out as a simple chart grid for look-and-feel inspection. These are + * manual reference specs (see cartoon-lab-data.ts), NOT theme-pipeline output; + * they explore what actually reads as *fun* before we commit to a `cartoon` + * ThemeSpec preset (a first pipeline attempt felt flat). + */ + +import { siteTheme } from '../shared/theme'; +import { VegaLiteView } from '../components/VegaLiteView'; +import { CARTOON_CASES } from './cartoon-lab-data'; + +export function CartoonLab() { + return ( +
+
+

+ Cartoon lab · hand-authored mockups +

+

+ Manual Vega-Lite specs exploring a playful, xkcd-flavoured look — a rounded + comic face, a bright crayon palette on warm paper, fat dark "sticker" outlines + on rounded marks, big haloed dots, and emoji markers. These are a design target + for a future cartoon preset, not theme-pipeline output: the first + pipeline pass felt flat, so we're testing which levers actually make it fun + (and which — like emoji markers — the theme spec would need to grow). +

+
+ +
+ {CARTOON_CASES.map((c) => ( +
+
+ +
+
+
+ {c.title} + + {c.id} + +
+
{c.note}
+
+
+ ))} +
+
+ ); +} diff --git a/site/src/playground/PlaygroundShell.tsx b/site/src/playground/PlaygroundShell.tsx index edf35f83..aff6ae67 100644 --- a/site/src/playground/PlaygroundShell.tsx +++ b/site/src/playground/PlaygroundShell.tsx @@ -1,20 +1,57 @@ -import { NavLink, Link, Outlet } from 'react-router-dom'; +import { NavLink, Link, Outlet, useLocation } from 'react-router-dom'; import { siteTheme } from '../shared/theme'; import './playground.css'; -const pages = [ +type NavLeaf = { to: string; label: string }; +type NavEntry = NavLeaf | { group: string; children: NavLeaf[] }; + +const pages: NavEntry[] = [ { to: 'illustrations', label: 'Illustrations' }, { to: 'mcp-ui', label: 'MCP UI test' }, { to: 'labs', label: 'Labs' }, { to: 'demo-wall', label: 'Demo wall' }, - { to: 'theme-labs', label: 'Theme lab' }, - { to: 'theme-lab-r2', label: 'Theme lab R2' }, - { to: 'theme-lab-real', label: 'Theme lab real' }, - { to: 'theme-lab-gaps', label: 'Theme lab gaps' }, - { to: 'swiss-lab', label: 'Swiss lab' }, + { + group: 'Theme labs', + children: [ + { to: 'theme-labs', label: 'Theme lab' }, + { to: 'theme-lab-r2', label: 'Theme lab R2' }, + { to: 'theme-lab-real', label: 'Theme lab real' }, + { to: 'theme-lab-gaps', label: 'Theme lab gaps' }, + { to: 'swiss-lab', label: 'Swiss lab' }, + { to: 'cartoon-lab', label: 'Cartoon lab' }, + ], + }, { to: 'full-test-cases', label: 'Full test cases' }, ]; +function ThemeLabsMenu({ group, children }: { group: string; children: NavLeaf[] }) { + const { pathname } = useLocation(); + const active = children.some((c) => pathname.endsWith(`/${c.to}`)); + return ( +
+ +
+ {children.map((page) => ( + isActive ? 'dev-nav-link dev-nav-link-active' : 'dev-nav-link'} + > + {page.label} + + ))} +
+
+ ); +} + export function PlaygroundShell() { return (
@@ -24,13 +61,17 @@ export function PlaygroundShell() {
diff --git a/site/src/playground/PlaygroundShell.tsx b/site/src/playground/PlaygroundShell.tsx index aff6ae67..5ad0ca07 100644 --- a/site/src/playground/PlaygroundShell.tsx +++ b/site/src/playground/PlaygroundShell.tsx @@ -16,7 +16,6 @@ const pages: NavEntry[] = [ { to: 'theme-labs', label: 'Theme lab' }, { to: 'theme-lab-r2', label: 'Theme lab R2' }, { to: 'theme-lab-real', label: 'Theme lab real' }, - { to: 'theme-lab-gaps', label: 'Theme lab gaps' }, { to: 'swiss-lab', label: 'Swiss lab' }, { to: 'cartoon-lab', label: 'Cartoon lab' }, ], diff --git a/site/src/playground/ThemeLabGaps.tsx b/site/src/playground/ThemeLabGaps.tsx deleted file mode 100644 index 76a9a086..00000000 --- a/site/src/playground/ThemeLabGaps.tsx +++ /dev/null @@ -1,71 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -/** - * Theme lab — round 2 gaps. - * - * Cases the coverage round found too particular to generalise from: a fix that - * only helped one of them would be a hack, not a fix (doc 05 §3). They are - * parked here with the reason they resist a general rule, so a human can decide - * whether the schema should grow to meet them. Empty until the round finds one. - */ - -import { siteTheme } from '../shared/theme'; -import { R2_CASES } from './theme-lab-r2-data'; -import { GAP_NOTES } from './theme-lab-gaps-data'; -import { R2Cell, R2_COLUMNS } from './ThemeLabR2Cell'; - -export function ThemeLabGaps() { - return ( -
-
-

- Theme lab · round 2 gaps -

-

- Cases parked for inspection — too particular for a general rule to reach without - becoming a per-chart hack. Each carries the reason it resists. -

-
- - {GAP_NOTES.length === 0 ? ( -

- No gaps parked yet. -

- ) : ( - GAP_NOTES.map((g, i) => { - const c = R2_CASES.find((x) => x.id === g.id); - const columns = g.theme ? (['flint', g.theme] as const) : R2_COLUMNS; - return ( -
-
-
- {g.id} - {g.theme ? · {g.theme} : null} - {c ? — {c.title} : null} -
-
{g.note}
-
- {c ? ( -
- {columns.map((col) => ( - - ))} -
- ) : ( -

unknown case `{g.id}`

- )} -
- ); - }) - )} -
- ); -} diff --git a/site/src/playground/cartoon-lab-data.ts b/site/src/playground/cartoon-lab-data.ts index 9517ffde..4236c52d 100644 --- a/site/src/playground/cartoon-lab-data.ts +++ b/site/src/playground/cartoon-lab-data.ts @@ -5,15 +5,14 @@ * Cartoon lab — hand-authored Vega-Lite mockups of a playful, colourful * "cartoon" look, in the spirit of xkcd / modern flat illustration. * - * These are NOT theme-pipeline output. They are manual reference specs used to - * explore what actually reads as *fun* before we commit to a `cartoon` - * ThemeSpec preset. A first pipeline attempt (font + palette + rounded bars) - * felt flat, so this lab pushes the levers a theme could eventually own AND a - * few it may need to grow: thick dark "sticker" outlines on rounded marks, big - * haloed dots, wobble-free but chunky strokes, emoji markers, and a warm paper - * canvas with a bright crayon palette. + * These are NOT theme-pipeline output. They are manual reference specs for the + * shipped `cartoon` ThemeSpec preset. A first pipeline attempt (font + palette + * + rounded bars) felt flat, so this lab established the reusable levers the + * preset now owns AND a few it still intentionally does not: thick dark + * "sticker" outlines on rounded marks and dots, wobble-free but chunky strokes, + * emoji markers, and a warm paper canvas with a bright crayon palette. * - * The design tokens we're testing (candidates for the eventual preset): + * The design tokens the preset is checked against: * - Type: a rounded comic face (Comic Sans / Comic Neue / Chalkboard). * - Palette: bright crayon — sky, coral, sunflower, grass, grape, tangerine. * - Furniture: warm cream paper, dashed soft grid, round-capped axes. diff --git a/site/src/playground/theme-lab-gaps-data.ts b/site/src/playground/theme-lab-gaps-data.ts deleted file mode 100644 index df769ed4..00000000 --- a/site/src/playground/theme-lab-gaps-data.ts +++ /dev/null @@ -1,74 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -/** - * Round-2 gaps: cases parked for inspection. - * - * The coverage round's rule (doc 05 §3) is that a fix must generalise: a change - * that only helps one chart is not a fix, it is a gap. Such cases are not left - * silently broken and they are not papered over with a per-chart hack — they - * are named here, with the reason they resist a general rule, so a human can - * look and decide whether the schema should grow to meet them. - * - * A gap references an R2 case by `id` (see `theme-lab-r2-data.ts`). `theme` - * names the column the problem shows in, or is omitted when it is the shape of - * the case itself, across houses, that is the gap. - */ - -export interface GapNote { - /** R2 case id the gap is about. */ - id: string; - /** The column it shows in, if it is house-specific. */ - theme?: string; - /** One line: what is wrong, and why it will not generalise. */ - note: string; -} - -export const GAP_NOTES: GapNote[] = [ - { - id: 'scatter-color-n50', - note: - 'Fifty nominal series on colour. No house owns fifty distinct inks, so the ' - + 'colours cycle and the key stops being a key — the probe\'s own point. Two ' - + 'things go wrong and neither has a fix that stays general at this size: the ' - + 'palette overflows (colours repeat, so the legend cannot name a point by its ' - + 'ink), and the houses that prefer a top legend (nyt, economist) lay all fifty ' - + 'items in one row, blowing the width out and crushing the plot into a corner. ' - + 'A real fix is a legend that wraps or falls back by measured width, and a ' - + 'policy for "more series than inks" (suppress-and-note, or roll up to top-N + ' - + 'other) — both larger than a coverage-round patch, and easy to get wrong blind. ' - + 'Parked for a human to decide how far the schema should grow.', - }, - { - id: 'pie-25', - note: - 'Twenty-five slices — a pie past legibility by construction (its probe). The ' - + 'label smear is now fixed (Iteration 2 suppresses always-labels past the marks ' - + 'floor), but the palette still overflows: past ~24 categories the qualitative ' - + 'inks exhaust and the tail of slices falls back to grey. Inherent to a ' - + '25-slice pie; the honest answer is "do not draw this pie", which no theme ' - + 'rule can assert for the author. Parked as the reference case for palette ' - + 'exhaustion on part-to-whole.', - }, - { - id: 'grouped-color-continuous', - theme: 'datawrapper', - note: - 'Fifty numeric groups on a *sequential* ramp, drawn as filled bars. ' - + 'datawrapper\'s ramp runs from near-white to blue — right for a heatmap, ' - + 'where cell gaps outline even the palest cell, but on bars against a white ' - + 'plot the low end of the ramp is the plot surface, so half the bars vanish ' - + 'and the panel reads as blank. The other houses survive only because their ' - + 'ramps start darker; flint\'s purple low-end stays visible too. The real ' - + 'principle underneath is generalisable — a *filled* mark needs a contrast ' - + 'floor against the surface it sits on, and a sequential ramp whose low end ' - + 'meets that surface must be lifted, or the marks given a thin stroke to ' - + 'outline them (as the heatmap\'s gaps already do). But that touches every ' - + 'sequential-coloured chart (heatmaps most of all, where the pale end is ' - + 'deliberate) and needs the fill-vs-bordered-cell distinction drawn carefully; ' - + 'too big and too risky to land blind mid-round. Parked as the reference case ' - + 'for "ramp low-end washes out a fill mark against its surface", alongside a ' - + 'note that continuous colour on fifty grouped bars is a poor encoding to ' - + 'begin with.', - }, -]; From 0805d15b8ea406652cfa795090bcc370fc6cd9a7 Mon Sep 17 00:00:00 2001 From: Chenglong Wang Date: Sun, 2 Aug 2026 12:17:50 -0700 Subject: [PATCH 080/164] Close the last two gaps in Vega-Lite theme coverage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Auditing the 35 Vega-Lite templates against the R2, real-data and unit test corpora turned up two chart types the theme layer never really reached, and one held-out corpus hole that was hiding one of them. KPI Card was the only template with no R2 case, and it rendered identically in all nine houses: the template draws the whole tile in pixels, so every colour is a literal and nothing binds to the theme. The card frame and captions were already re-toned as furniture, but the progress bar is not furniture — it is the one part of the tile that states a measurement, and it stayed flint's blue everywhere. Templates cannot read the theme, so the bar now names the *role* its colour plays (`__themeRole`) and the house supplies the ink: its accent for a reading still in progress, its status inks for one with a verdict. Houses that never named status inks keep the template's conventional red and green rather than have a verdict colour invented for them. A verdict also has to stay legible as a verdict. Several houses draw status ink from the same short palette as series ink — the Economist's positive is its blue, Swiss's negative is its red — so taking it unconditionally painted "beat the target" and "still going" in one colour. Where the house's verdict ink is already the ink on the bar, the template's hue stays. The second gap was structural rather than per-chart. Every rule that budgets a mark against available room read a single `plotWidth` off the top-level spec, but a composed chart has no single plot: the Cartoon sparkline's line panel is 114px inside a 226px chart, so its dots were budgeted against a plot twice their real size. Measured on the scenegraph, the reading spacing was 9.5px and the dot 17.2px edge to edge — the beads fused into a rope and hid the line they sat on. `walkScoped` carries the nearest declared view size down the tree, and the crowding guard now measures the dot against the gap between two readings rather than counting readings alone: countable is not the same as roomy. Where the dot does not fit it shrinks on the *diameter* (so the house's dot-to-line proportion survives), and stands down only if it still collides at the floor. Cartoon's sparkline dot now sits at 0.92 of its spacing, against Nature's 0.86 on the same chart. Diffing every line mark across both corpora — 372 theme/case pairs — this moves exactly three, all Cartoon, all in panels under 120px. No other house or case changes. Adds a KPI Card generator with cases spanning all three verdict states, and the two R2 cases that close the corpus at 35/35. Full R2 (87 sheets) and real-data (59 sheets) renders complete with no failures. Funnel and Gauge remain Vega-Lite gaps — they exist for ECharts, Plotly and Excel, and the harness still skips them. Left for separate work. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 175e0e03-5dda-4a02-aa14-c181660b9c99 --- .../src/test-data/gantt-bullet-tests.ts | 63 ++++++ packages/flint-js/src/test-data/index.ts | 5 +- .../src/vegalite/templates/kpi-card.ts | 9 + packages/flint-js/src/vegalite/theme.ts | 181 +++++++++++++++++- packages/flint-js/tests/theme-presets.test.ts | 139 ++++++++++++++ site/src/playground/theme-lab-r2-data.ts | 2 + 6 files changed, 390 insertions(+), 9 deletions(-) diff --git a/packages/flint-js/src/test-data/gantt-bullet-tests.ts b/packages/flint-js/src/test-data/gantt-bullet-tests.ts index 34d9a210..950aadbe 100644 --- a/packages/flint-js/src/test-data/gantt-bullet-tests.ts +++ b/packages/flint-js/src/test-data/gantt-bullet-tests.ts @@ -190,3 +190,66 @@ export function genBulletTests(): TestCase[] { ...realBulletCases(), ]; } + +// --------------------------------------------------------------------------- +// KPI Card — big-number tiles, each measured against its own goal +// --------------------------------------------------------------------------- + +/** metric, value, goal — chosen to land one tile in each verdict state. */ +const QUARTER_KPIS: Array<[string, number, number]> = [ + ['Revenue ($k)', 1284, 1200], // exceeded — at or above goal + ['New customers', 372, 500], // on track — between the two + ['Churn saves', 41, 120], // behind — well short of goal + ['NPS', 54, 50], // exceeded +]; + +const ADOPTION_KPIS: Array<[string, number, number]> = [ + ['Weekly actives (k)', 88, 120], + ['Seats licensed (k)', 143, 140], +]; + +export function genKpiCardTests(): TestCase[] { + const quarter = QUARTER_KPIS.map(([metric, value, goal]) => ({ metric, value, goal })); + const adoption = ADOPTION_KPIS.map(([metric, value, goal]) => ({ metric, value, goal })); + const meta = { + metric: { type: Type.String, semanticType: 'Category', levels: [] }, + value: { type: Type.Number, semanticType: 'Quantity', levels: [] }, + goal: { type: Type.Number, semanticType: 'Quantity', levels: [] }, + }; + const encodingMap = { + metric: makeEncodingItem('metric'), + value: makeEncodingItem('value'), + goal: makeEncodingItem('goal'), + }; + const fields = [makeField('metric'), makeField('value'), makeField('goal')]; + return [ + { + title: 'Quarterly KPIs vs goal', + description: + 'Four big-number tiles, each with a progress bar against its own ' + + 'goal. The four deliberately span every verdict the card can ' + + 'reach — two that beat their goal, one still in progress and one ' + + 'well short — so a theme\u2019s accent and its status inks all ' + + 'appear on a single sheet and can be told apart.', + tags: ['kpi', 'card', 'big-number', 'target', 'gallery'], + chartType: 'KPI Card', + data: quarter, + fields, + metadata: meta, + encodingMap, + }, + { + title: 'Adoption against plan', + description: + 'A two-tile card: one metric short of plan and one just past it, ' + + 'at the width where the tiles are widest and the big number has ' + + 'the most room.', + tags: ['kpi', 'card', 'big-number', 'target', 'gallery'], + chartType: 'KPI Card', + data: adoption, + fields, + metadata: meta, + encodingMap, + }, + ]; +} diff --git a/packages/flint-js/src/test-data/index.ts b/packages/flint-js/src/test-data/index.ts index 841f0f6f..e59fc3ad 100644 --- a/packages/flint-js/src/test-data/index.ts +++ b/packages/flint-js/src/test-data/index.ts @@ -45,7 +45,7 @@ export { genDiscreteAxisTests } from './discrete-axis-tests'; export { genDateTests, genDateYearTests, genDateMonthTests, genDateYearMonthTests, genDateDecadeTests, genDateDateTimeTests, genDateHoursTests } from './date-tests'; export { genSemanticContextTests, genSnapToBoundTests } from './semantic-tests'; export { genMapTests, genChoroplethTests } from './map-tests'; -export { genGanttTests, genBulletTests } from './gantt-bullet-tests'; +export { genGanttTests, genBulletTests, genKpiCardTests } from './gantt-bullet-tests'; export { OMNI_VIZ_ROWS, OMNI_VIZ_LEVELS, @@ -92,7 +92,7 @@ import { genHistogramTests, genBoxplotTests, genDensityTests, genStripPlotTests import { genDensityContourTests } from './density-2d-tests'; import { genViolinTests } from './violin-tests'; import { genMapTests, genChoroplethTests } from './map-tests'; -import { genGanttTests, genBulletTests } from './gantt-bullet-tests'; +import { genGanttTests, genBulletTests, genKpiCardTests } from './gantt-bullet-tests'; import { genLineTests } from './line-tests'; import { genSparklineTests } from './sparkline-tests'; import { genBumpChartTests } from './line-area-tests'; @@ -180,6 +180,7 @@ export const TEST_GENERATORS: Record TestCase[]> = { 'Choropleth': genChoroplethTests, 'Gantt Chart': genGanttTests, 'Bullet Chart': genBulletTests, + 'KPI Card': genKpiCardTests, 'Facet: Columns': genFacetColumnTests, 'Facet: Rows': genFacetRowTests, 'Facet: Cols+Rows': genFacetColRowTests, diff --git a/packages/flint-js/src/vegalite/templates/kpi-card.ts b/packages/flint-js/src/vegalite/templates/kpi-card.ts index be2ffb2a..e68df82e 100644 --- a/packages/flint-js/src/vegalite/templates/kpi-card.ts +++ b/packages/flint-js/src/vegalite/templates/kpi-card.ts @@ -372,6 +372,10 @@ export const kpiCardDef: ChartTemplateDef = { : PROGRESS_ON_TRACK; layers.push({ + // Only the exceeded state paints this line a status hue; + // otherwise it is ordinary caption grey and re-tones with + // the rest of the card's text. + ...(isExceeded ? { __themeRole: 'positive' } : {}), data: { values: [{}] }, mark: { type: 'text', @@ -410,6 +414,11 @@ export const kpiCardDef: ChartTemplateDef = { // that the goal was exceeded. const fillEnd = barLeft + Math.min(1, pct) * barWidth; layers.push({ + // The bar is the only part of the card that carries a + // measurement, so it takes the house's ink: its accent + // where the reading is simply in progress, and the + // house's status inks where the reading has a verdict. + __themeRole: isExceeded ? 'positive' : isBehind ? 'negative' : 'accent', data: { values: [{}] }, mark: { type: 'rect', diff --git a/packages/flint-js/src/vegalite/theme.ts b/packages/flint-js/src/vegalite/theme.ts index 72b16137..2c7990a9 100644 --- a/packages/flint-js/src/vegalite/theme.ts +++ b/packages/flint-js/src/vegalite/theme.ts @@ -172,6 +172,35 @@ function walk(node: any, visit: (n: any) => void): void { if (node.facet && node.facet.spec) walk(node.facet.spec, visit); } +/** + * As {@link walk}, but carrying the size of the view each node is drawn in. + * + * A composed spec has no single plot: a concat gives each panel its own width, + * and a sparkline's line panel is a third of the chart it sits in. Anything + * that budgets a mark against the space available to it — how much room a dot + * has before it touches its neighbour — has to ask the view the mark is + * actually drawn in, not the outermost one, or a panel is measured against a + * plot several times its size. The nearest declared numeric `width`/`height` + * wins; children inherit until one of them overrides it. + */ +function walkScoped( + node: any, + outer: { width?: number; height?: number }, + visit: (n: any, view: { width?: number; height?: number }) => void, +): void { + if (!node || typeof node !== 'object') return; + const view = { + width: typeof node.width === 'number' ? node.width : outer.width, + height: typeof node.height === 'number' ? node.height : outer.height, + }; + visit(node, view); + for (const key of ['layer', 'vconcat', 'hconcat', 'concat']) { + if (Array.isArray(node[key])) node[key].forEach((c: any) => walkScoped(c, view, visit)); + } + if (node.spec) walkScoped(node.spec, view, visit); + if (node.facet && node.facet.spec) walkScoped(node.facet.spec, view, visit); +} + /** * The node that owns the plot body — where layers must be added and where the * positional encodings live. For a facet spec that is `spec`, for a concat it @@ -949,6 +978,11 @@ function applyMarks(spec: any, d: DesignDecisions, table: any[], say: (p: string } protectDashEncoding(spec, config, m.strokeWidth); + // Dots whose size this house deliberately cut to fit the room its panel + // gives them. The ring-writing passes below hand a bare `point: true` the + // house's full-size ring, which would put back exactly the ink the fit + // just took out, so they leave these alone. + const fittedDots = new Set(); if (m.point?.show) { config.line.point = { filled: m.point.filled !== false, @@ -981,18 +1015,56 @@ function applyMarks(spec: any, d: DesignDecisions, table: any[], say: (p: string // A spec that asked for points itself keeps them; this only decides // whether the *house* adds them where the chart never asked. let saidCrowd = false; - walk(spec, (node) => { + let saidShrink = false; + walkScoped(spec, { width: plotWidth, height: plotHeight }, (node, view) => { if (!LINE_MARKS.has(markTypeOf(node.mark) ?? '')) return; const mark = normalizeMark(node.mark); if (mark.point !== undefined) return; const enc = mergedEncoding(node, spec.encoding); const readings = maxReadingsPerSeries(enc, table); - if (readings <= MAX_DOTTED_READINGS) return; - node.mark = { ...mark, point: false }; - if (!saidCrowd) { - say('marks.point', - `${readings} readings on one line is past the ${MAX_DOTTED_READINGS} a reader can take one at a time, so the house's dots stand down and the line keeps its shape`); - saidCrowd = true; + if (readings > MAX_DOTTED_READINGS) { + node.mark = { ...mark, point: false }; + if (!saidCrowd) { + say('marks.point', + `${readings} readings on one line is past the ${MAX_DOTTED_READINGS} a reader can take one at a time, so the house's dots stand down and the line keeps its shape`); + saidCrowd = true; + } + return; + } + // Countable is not the same as roomy. A dozen readings are easy to + // take one at a time across a full plot and impossible across a + // sparkline a third as wide, where the house's dot is wider than + // the gap between two of them. Measure the dot against the space + // this view actually gives it and shrink it until it fits. + const dot = config.line.point; + const spacing = readingSpacing(view.width, readings); + if (spacing == null) return; + const outer = dotOuterDiameter(dot.size, dot.strokeWidth ?? 0); + const needed = (spacing * DOT_SPACING_FIT) / outer; + if (needed >= 1) return; + const shrink = Math.max(MIN_DOT_SHRINK, needed); + if (outer * shrink > spacing) { + // Even at the smallest a bead may be drawn it still runs into + // its neighbour, so the line is better read without them. + node.mark = { ...mark, point: false }; + if (!saidCrowd) { + say('marks.point', + `${readings} readings across ${Math.round(spacing)}px apart leave no room for the house's ${Math.round(outer)}px dots, so they stand down and the line keeps its shape`); + saidCrowd = true; + } + return; + } + const size = Math.max(4, Math.round(dot.size * shrink * shrink)); + const point: any = { ...dot, size }; + if (dot.strokeWidth) { + point.strokeWidth = Math.max(0.5, Number((dot.strokeWidth * shrink).toFixed(1))); + } + node.mark = { ...mark, point }; + fittedDots.add(node); + if (!saidShrink) { + say('marks.point.size', + `this panel gives each of its ${readings} readings ${Math.round(spacing)}px, so the house's dots shrink to ${size}px² to sit apart on their line`); + saidShrink = true; } }); } @@ -1006,6 +1078,7 @@ function applyMarks(spec: any, d: DesignDecisions, table: any[], say: (p: string const dot = m.point; walk(spec, (node) => { if (!LINE_MARKS.has(markTypeOf(node.mark) ?? '')) return; + if (fittedDots.has(node)) return; const mark = normalizeMark(node.mark); if (!mark.point) return; mark.point = { @@ -1021,6 +1094,7 @@ function applyMarks(spec: any, d: DesignDecisions, table: any[], say: (p: string if (m.outline && !m.point?.haloColor) { walk(spec, (node) => { if (!LINE_MARKS.has(markTypeOf(node.mark) ?? '')) return; + if (fittedDots.has(node)) return; const mark = normalizeMark(node.mark); if (!mark.point) return; mark.point = { @@ -1695,9 +1769,99 @@ function distinctCount(table: any[], field: string | undefined): number { return seen.size; } +/** + * Paint the marks a template drew in pixels but flagged as carrying meaning. + * + * Most template furniture is scenery — a card, a track, a caption — and the + * pass above only re-tones it against the surface. A few of those pixel marks + * are not scenery at all: a KPI card's progress bar is the one part of the + * tile that states a measurement, and left literal it stays flint's blue in + * every house on the wall. A template cannot read the theme, so it names the + * *role* its colour plays with `__themeRole` and the house supplies the ink. + * + * accent the reading itself, with no verdict attached — the house's ink + * positive a reading that met or beat its target + * negative a reading that fell short + * + * A house that never named status inks keeps the template's own red and green: + * those hues are conventional rather than decorative, and inventing a verdict + * colour from the palette would say something the house did not. + * + * A verdict also has to stay *legible as a verdict*. Several houses draw their + * status ink from the same short palette as their series ink — the Economist's + * positive is its blue, Swiss's negative is its red — so taking it here would + * paint "beat the target" and "still going" in one colour and quietly delete + * the distinction the bar exists to make. Where the house's verdict ink is the + * ink already on the bar, the template's conventional hue stays. + */ +function paintRoleMarks(spec: any, d: DesignDecisions, say: (p: string, m: string) => void): void { + const status = d.series.status; + const accent = d.series.single; + const same = (a?: string, b?: string) => !!a && !!b && a.toLowerCase() === b.toLowerCase(); + const verdict = (ink?: string) => (ink && !same(ink, accent) ? ink : undefined); + let said = false; + walk(spec, (node) => { + const role = node.__themeRole; + if (!role || !markTypeOf(node.mark)) return; + const ink = role === 'accent' + ? accent + : role === 'positive' + ? verdict(status?.positive) + : role === 'negative' + ? verdict(status?.negative) + : undefined; + if (!ink) return; + const mark = normalizeMark(node.mark); + for (const key of ['fill', 'stroke', 'color'] as const) { + if (typeof mark[key] === 'string') mark[key] = ink; + } + node.mark = mark; + if (!said) { + say('ink.series', + 'the card\'s progress bar states a measurement, not scenery, so it takes the house ink rather than the template\'s own blue'); + said = true; + } + }); +} + + + /** How many dots one line can carry before they stop being countable. */ const MAX_DOTTED_READINGS = 12; +/** + * The share of the gap between two readings a dot may occupy. + * + * At 1 the beads on a line touch exactly; a little under that leaves a thread + * of page between them, which is what makes them read as separate readings + * rather than as one rope. + */ +const DOT_SPACING_FIT = 0.9; + +/** + * The room one reading gets along a line, in px. + * + * Returns `undefined` when the view has no settled width (a step-sized or + * responsive panel), where there is nothing to measure the dot against and the + * house's size is left alone. + */ +function readingSpacing(width: number | undefined, readings: number): number | undefined { + if (typeof width !== 'number' || !isFinite(width) || width <= 0) return undefined; + if (readings <= 1) return undefined; + return width / readings; +} + +/** + * How much page a dot covers, edge to edge. + * + * A mark's `size` is its area in px², and its stroke straddles the edge it is + * drawn on, so half the stroke falls outside the disc on each side — the ink a + * neighbouring dot has to clear is the diameter plus one whole stroke width. + */ +function dotOuterDiameter(size: number, strokeWidth: number): number { + return 2 * Math.sqrt(size / Math.PI) + strokeWidth; +} + /** * The most of a bar's own thickness a rounded end may eat. * @@ -2197,6 +2361,7 @@ function applySeriesInk(spec: any, d: DesignDecisions, table: any[], say: (p: st // stays a white island on a dark canvas. walk(spec, (node) => { if (!markTypeOf(node.mark) || node.__themeSynthetic || !isLiteralMark(node)) return; + if (node.__themeRole) return; const mark = normalizeMark(node.mark); let changed = false; for (const key of ['fill', 'stroke', 'color'] as const) { @@ -2214,6 +2379,8 @@ function applySeriesInk(spec: any, d: DesignDecisions, table: any[], say: (p: st } }); + paintRoleMarks(spec, d, say); + walk(spec, (node) => { const mark = markTypeOf(node.mark); if (!mark || !DATA_MARKS.has(mark)) return; diff --git a/packages/flint-js/tests/theme-presets.test.ts b/packages/flint-js/tests/theme-presets.test.ts index b76c60e7..ac71d21b 100644 --- a/packages/flint-js/tests/theme-presets.test.ts +++ b/packages/flint-js/tests/theme-presets.test.ts @@ -869,3 +869,142 @@ describe('keeping a value key when there is no series legend', () => { expect(size.legend).not.toBeNull(); }); }); + +/** + * A composed chart has no single plot. Anything that budgets a mark against + * the room it has must ask the view the mark is drawn in, not the outermost + * one, or a narrow panel is measured against a plot several times its size. + */ +describe('vertex dots are budgeted against the panel that holds them', () => { + const MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; + const SPARK = MONTHS.flatMap((Month, i) => + ['Revenue', 'Users'].map((Metric) => ({ Metric, Month, Value: 10 + i + (Metric === 'Users' ? 5 : 0) })), + ); + + function walkScoped(node: any, outerW: number | undefined, visit: (n: any, w?: number) => void): void { + if (!node || typeof node !== 'object') return; + const w = typeof node.width === 'number' ? node.width : outerW; + visit(node, w); + for (const key of ['layer', 'concat', 'hconcat', 'vconcat']) { + if (Array.isArray(node[key])) node[key].forEach((c: any) => walkScoped(c, w, visit)); + } + if (node.spec) walkScoped(node.spec, w, visit); + if (node.facet?.spec) walkScoped(node.facet.spec, w, visit); + } + + /** Edge-to-edge ink of a dot: `size` is an area, and the stroke straddles. */ + function outerDiameter(size: number, strokeWidth: number): number { + return 2 * Math.sqrt(size / Math.PI) + strokeWidth; + } + + function sparkline(themeId: string): any { + return assembleVegaLite({ + data: { values: SPARK }, + semantic_types: { Metric: 'Category', Month: 'Category', Value: 'Quantity' }, + chart_spec: { + chartType: 'Sparkline', + encodings: { x: 'Month', y: 'Value', color: 'Metric' }, + baseSize: { width: 300, height: 300 }, + }, + theme_spec: THEME_PRESETS[themeId].spec, + } as any) as any; + } + + it('shrinks the cartoon dot so it fits the gap between two readings', () => { + const spec = sparkline('cartoon'); + let checked = 0; + walkScoped(spec, spec._width, (node, width) => { + const mark = node.mark; + if (markTypeOf(mark) !== 'line' || typeof mark !== 'object') return; + const dot = mark.point ?? spec.config?.line?.point; + if (!dot || typeof dot !== 'object') return; + expect(typeof width).toBe('number'); + // One reading per month along this panel. + const spacing = (width as number) / MONTHS.length; + expect(outerDiameter(dot.size, dot.strokeWidth ?? 0)).toBeLessThanOrEqual(spacing); + checked++; + }); + expect(checked).toBeGreaterThan(0); + }); + + it('leaves the dot alone where the panel is wide enough to hold it', () => { + // The same house and the same twelve readings across a full-width plot + // keep the authored size — the rule reacts to room, not to count. + const spec = assembleVegaLite({ + data: { values: MONTHS.map((Month, i) => ({ Month, Value: 10 + i })) }, + semantic_types: { Month: 'Month', Value: 'Quantity' }, + chart_spec: { + chartType: 'Line Chart', + encodings: { x: 'Month', y: 'Value' }, + baseSize: { width: 600, height: 300 }, + }, + theme_spec: THEME_PRESETS['cartoon'].spec, + } as any) as any; + const authored = THEME_PRESETS['cartoon'].spec.marks?.point?.size; + expect(spec.config.line.point.size).toBe(authored); + let overridden = false; + walkScoped(spec, spec._width, (node) => { + const mark = node.mark; + if (markTypeOf(mark) !== 'line' || typeof mark !== 'object') return; + if (mark.point && typeof mark.point === 'object' && mark.point.size != null) overridden = true; + }); + expect(overridden).toBe(false); + }); +}); + +/** + * A KPI card is drawn entirely in pixels, so none of it reaches the theme + * through an encoding. Its progress bar is nonetheless the one part of the + * tile that states a measurement, and it has to carry the house's ink. + */ +describe('KPI card progress bar takes house ink', () => { + const KPIS = [ + { metric: 'Behind', value: 20, goal: 100 }, + { metric: 'On track', value: 70, goal: 100 }, + { metric: 'Exceeded', value: 130, goal: 100 }, + ]; + + function card(themeId: string): any { + return assembleVegaLite({ + data: { values: KPIS }, + semantic_types: { metric: 'Category', value: 'Quantity', goal: 'Quantity' }, + chart_spec: { + chartType: 'KPI Card', + encodings: { metric: 'metric', value: 'value', goal: 'goal' }, + baseSize: { width: 600, height: 240 }, + }, + theme_spec: THEME_PRESETS[themeId].spec, + } as any) as any; + } + + function bars(spec: any): Record { + const out: Record = {}; + const visit = (n: any): void => { + if (!n || typeof n !== 'object') return; + if (Array.isArray(n)) { n.forEach(visit); return; } + if (n.__themeRole && n.mark?.type === 'rect') out[n.__themeRole] = n.mark.fill; + for (const k of Object.keys(n)) visit(n[k]); + }; + visit(spec); + return out; + } + + for (const id of Object.keys(THEME_PRESETS)) { + it(`${id}: the in-progress bar is the house's own ink`, () => { + const spec = card(id); + const found = bars(spec); + expect(found.accent).toBe(THEME_PRESETS[id].spec.ink?.series?.single); + }); + + it(`${id}: met and missed stay distinguishable from in-progress`, () => { + const found = bars(card(id)); + expect(found.accent).toBeTruthy(); + expect(found.positive).toBeTruthy(); + expect(found.negative).toBeTruthy(); + // A verdict painted the same ink as "still going" says nothing. + expect(found.positive).not.toBe(found.accent); + expect(found.negative).not.toBe(found.accent); + expect(found.positive).not.toBe(found.negative); + }); + } +}); diff --git a/site/src/playground/theme-lab-r2-data.ts b/site/src/playground/theme-lab-r2-data.ts index 0828d5d4..b2773fdc 100644 --- a/site/src/playground/theme-lab-r2-data.ts +++ b/site/src/playground/theme-lab-r2-data.ts @@ -154,6 +154,8 @@ export const R2_CASES: R2Case[] = [ { id: 'gantt-project', gen: 'Gantt Chart', index: 0, family: 'Single value & schedule', title: 'Project schedule', probe: 'a temporal range per row' }, { id: 'gantt-ci', gen: 'Gantt Chart', index: 1, family: 'Single value & schedule', title: 'Pipeline run', subtitle: 'Seconds from start', probe: 'a numeric range per row' }, { id: 'bullet-12', gen: 'Bullet Chart', index: 1, family: 'Single value & schedule', title: 'Revenue against target', subtitle: 'Twelve stores', probe: 'twelve rows of measure, target and bands' }, + { id: 'kpi-verdicts', gen: 'KPI Card', index: 0, family: 'Single value & schedule', title: 'Quarterly KPIs', subtitle: 'Against goal', probe: 'a chart drawn entirely in pixels — whether the house reaches its progress bar, and whether accent, met and missed stay three distinguishable inks' }, + { id: 'kpi-pair', gen: 'KPI Card', index: 1, family: 'Single value & schedule', title: 'Adoption against plan', probe: 'two wide tiles — the big number at its largest, and card furniture re-toned to the house surface' }, ]; /** The base canvas every R2 case is designed at, so sheets are comparable. */ From 2556cd8537a029a7bba08232dee381cf2059c3db Mon Sep 17 00:00:00 2001 From: Chenglong Wang Date: Sun, 2 Aug 2026 19:47:32 -0700 Subject: [PATCH 081/164] theme: dumbbell connectors, plot edges, dividers, dark-surface legibility MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Six fixes to how a house's decisions land on the plot. Ranged dot connectors. `ground.ts` only resolved a connector when the house declared one, so six houses left the bridge on the series ink — which is the Min dot's own colour, making the span read as belonging to one of its two ends. The connector now always resolves; `show` means the house styles it, and a silent house gets bridges repainted (colour only, not width or dash) in a neutral measured from the three houses that do declare one: 0.45 of the way from the axis-label ink toward the plot. Marks that already state a stroke, or whose colour is encoded, are left alone — that also drops some dead overrides on radar spokes and candle wicks. Strip plots keep their baseline. The floating-value-scale rule dropped a banded axis's domain on the reasoning that a rule under the categories claims a base the chart does not have. That argument only holds where marks are read as lengths from that line. A strip plot's dots, a dumbbell's dots and a bump chart's lines are read by position, and for them the line is a wall like the left spine. Narrowed to charts that actually measure from it. Dots get room. A continuous position scale now takes padding equal to the dot's outer radius, so an extreme reading is not half-clipped by the plot edge. Paired with `nice: false`, since `nice` otherwise rounds the padding out to a whole tick. Off wherever a bar, area, rect, rule or arc measures from the edge. Grid no longer repaints a spine. In a faceted plot Vega-Lite hoists the shared axis out of the panels, so the panel grid is always drawn after the domain and z-order cannot help. The gridline that lands on the axis end is made transparent through a conditional axis property instead: the spine already states that edge. Group dividers centre. The dashed rule between groups of boxes was written at `bandPosition: 1`, the band's end — the middle of the gap only while the gap is nothing. Once a house opens `paddingInner` it sat hard against the left group's shoulder. It now moves half a gap further. Legibility on dark surfaces. A box plot whose colour is encoded leaves Vega-Lite drawing the whiskers literal black, which vanishes on Power BI dark; they take the text ink, which on a light house is the black they already were. Radar axis labels, written by the template as text marks and positioned by fields, escaped the furniture re-tone and kept their literal grey; template-written labels now take the house label ink. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 175e0e03-5dda-4a02-aa14-c181660b9c99 --- packages/flint-js/src/core/theme/ground.ts | 65 ++-- packages/flint-js/src/vegalite/theme.ts | 330 +++++++++++++++++- packages/flint-js/tests/theme-presets.test.ts | 319 +++++++++++++++++ 3 files changed, 677 insertions(+), 37 deletions(-) diff --git a/packages/flint-js/src/core/theme/ground.ts b/packages/flint-js/src/core/theme/ground.ts index fcd3cc8d..f3cf9d2c 100644 --- a/packages/flint-js/src/core/theme/ground.ts +++ b/packages/flint-js/src/core/theme/ground.ts @@ -1198,33 +1198,50 @@ export function groundTheme(themeIn: ThemeSpec, ctx: GroundingContext): DesignDe : plot, } : undefined, - connector: marksSpec.connector - ? { - show: (marksSpec.connector.presence ?? 'omit') !== 'omit', - // A connector is not a gridline. It borrows the rule's ink - // where the house declares none of its own, but it is read as - // part of the mark, not through it — so it is scaled at the - // step the house named against whichever of the two inks it - // gave, and a house whose rules are already pale states a - // `connector` ink rather than fading a faint grey further. - color: ink( + connector: { + // A house that names no connector has still not asked for its + // dumbbell's bridge to be drawn in a series colour. `show` says + // whether the house styles connectors at all; stage 3 uses it to + // separate the roles it may restyle freely from the one it must + // correct either way. The ink is resolved for both cases, so an + // undeclared house gets the same quiet structural grey a declared + // one would have got by saying nothing about its ink. + show: marksSpec.connector ? (marksSpec.connector.presence ?? 'omit') !== 'omit' : false, + // A connector is not a gridline. It borrows the rule's ink + // where the house declares none of its own, but it is read as + // part of the mark, not through it — so it is scaled at the + // step the house named against whichever of the two inks it + // gave, and a house whose rules are already pale states a + // `connector` ink rather than fading a faint grey further. + // + // Where the house declares no connector at all there is no step + // to scale and no ink to borrow that is not the grid's, and grid + // ink is too faint: a bridge carries the reading, so it has to + // sit clearly above the lines drawn *through* the plot even while + // it stays below the marks. The three houses that do state a + // connector ink put it at very nearly the same place — about + // 45% of the way from the axis-label ink toward the plot surface + // (mckinsey 0.48, powerbi 0.47, powerbi-light 0.26) — so a silent + // house is given the same relationship against its own two inks. + color: marksSpec.connector + ? ink( marksSpec.connector.presence, structureInk.connector ?? structureInk.rule, 'quiet', - ) ?? undefined, - width: marksSpec.connector.weight ?? 1, - // A stem and a bridge are one setting only in the sense that - // both are drawn in structure's ink. What they are worth - // differs: a stem repeats a position already plotted, a bridge - // draws a distance that is plotted nowhere else. So a house - // that says nothing about the bridge is not silent about it - // either — it has already said what a mark of its own weighs. - spanWidth: marksSpec.connector.spanWeight ?? (marksSpec.strokeWeight ?? 2), - ...(marksSpec.connector.style && marksSpec.connector.style !== 'solid' - ? { dash: marksSpec.connector.style === 'dotted' ? [1, 2] : [4, 3] } - : {}), - } - : undefined, + ) ?? undefined + : mixHex(axisLabelText.color ?? foreground, plot, 0.45, foreground), + width: marksSpec.connector?.weight ?? 1, + // A stem and a bridge are one setting only in the sense that + // both are drawn in structure's ink. What they are worth + // differs: a stem repeats a position already plotted, a bridge + // draws a distance that is plotted nowhere else. So a house + // that says nothing about the bridge is not silent about it + // either — it has already said what a mark of its own weighs. + spanWidth: marksSpec.connector?.spanWeight ?? (marksSpec.strokeWeight ?? 2), + ...(marksSpec.connector?.style && marksSpec.connector.style !== 'solid' + ? { dash: marksSpec.connector.style === 'dotted' ? [1, 2] : [4, 3] } + : {}), + }, interval: marksSpec.interval ? { fillOpacity: marksSpec.interval.fillOpacity, diff --git a/packages/flint-js/src/vegalite/theme.ts b/packages/flint-js/src/vegalite/theme.ts index 2c7990a9..9e275162 100644 --- a/packages/flint-js/src/vegalite/theme.ts +++ b/packages/flint-js/src/vegalite/theme.ts @@ -358,8 +358,18 @@ function applyAxes(spec: any, config: any, d: DesignDecisions, table: any[], say // itself a measure — a scatter's horizontal — its rule is the edge of // the window, not a base anything is measured from, and taking it away // leaves the plot hanging off a single wall. + // + // It is also an argument about *lengths*. The rule lies only if + // someone reads a distance from it, and only bars and areas are read + // that way. A strip plot's dots, a dumbbell's dots and a bump chart's + // lines are read against the tick labels; none of them reaches the + // bottom of the plot, so the line there is doing what the left-hand + // spine does — bounding the window and giving the category labels + // something to hang from. A house that draws one wall and not the + // other has not made a point about zero, it has lost a wall. let domainShow = axis.domain.show; - if (domainShow && axis.indexing && bandedAxis(spec, channel) && floatingValueScale(spec, channel)) { + if (domainShow && axis.indexing && bandedAxis(spec, channel) + && floatingValueScale(spec, channel) && lengthsFromIndexAxis(spec, channel)) { domainShow = false; say(`axes.${channel}.domain`, 'the value scale floats — a rule under the categories would claim a base the chart does not have'); @@ -573,6 +583,8 @@ function applyAxes(spec: any, config: any, d: DesignDecisions, table: any[], say } }); } + + dropGridUnderSpine(spec, config, say); } /** @@ -601,6 +613,32 @@ function bandedAxis(spec: any, channel: 'x' | 'y'): boolean { * whatever the scale asked for, and the rule under it is the floor those stems * land on. */ +/** + * True when something on the chart is read as a *length* measured from the + * index axis, so a rule drawn there would be the line that length starts at. + * + * Bars and areas are the marks that work this way: their reading is extent, + * and the base they extend from is wherever the plot ends. Everything else on + * a banded axis — dots, ticks, series lines — is read by position against the + * labels, and stops well short of the edge; the edge is then just the edge. + * + * A bar or area given a second value channel is a *span*, not a length from a + * base: a gantt task runs between two dates and a range band between two + * values, and neither is measured from the bottom of the plot either. + */ +function lengthsFromIndexAxis(spec: any, indexChannel: 'x' | 'y'): boolean { + const other = indexChannel === 'x' ? 'y' : 'x'; + let found = false; + walk(spec, (node) => { + if (found || isLiteralMark(node)) return; + const type = markTypeOf(node.mark); + if (type !== 'bar' && type !== 'area') return; + if (node.encoding?.[`${other}2`]) return; + found = true; + }); + return found; +} + function floatingValueScale(spec: any, indexChannel: 'x' | 'y'): boolean { const other = indexChannel === 'x' ? 'y' : 'x'; let floating = false; @@ -1142,6 +1180,16 @@ function applyMarks(spec: any, d: DesignDecisions, table: any[], say: (p: string } } + // Every house's dots need the room, not only the houses that size them: + // where none is stated the renderer still draws a 30px² dot with whatever + // outline the house asked for, and that dot has the same radius problem. + padScaleForDots( + spec, + config.point?.size ?? config.circle?.size ?? 30, + config.point?.strokeWidth ?? m.outline?.width ?? 0, + say, + ); + // Band occupancy is a scale decision, not a mark size: expressing it as // padding keeps grouped and simple bars consistent and leaves the layout // engine's step untouched. @@ -1154,6 +1202,7 @@ function applyMarks(spec: any, d: DesignDecisions, table: any[], say: (p: string const paddingInner = clamp(1 - m.bandFraction, 0, 0.9); let saidCells = false; let saidBand = false; + const paddedBandFields = new Set(); const bandWalk = (node: any, inherited: any): void => { if (!node || typeof node !== 'object') return; const enc = mergedEncoding(node, inherited); @@ -1199,6 +1248,7 @@ function applyMarks(spec: any, d: DesignDecisions, table: any[], say: (p: string const target = node.encoding?.[channel] ?? inherited?.[channel]; if (!target) continue; target.scale = { ...(target.scale ?? {}), paddingInner }; + if (typeof target.field === 'string') paddedBandFields.add(target.field); } // Dodged bars carry a second band inside each group — the // offset. A house gap that only narrows the group leaves the @@ -1272,6 +1322,7 @@ function applyMarks(spec: any, d: DesignDecisions, table: any[], say: (p: string if (node.facet?.spec) bandWalk(node.facet.spec, enc); }; bandWalk(spec, undefined); + centerBandDividers(spec, paddedBandFields, paddingInner, say); // A sized mark reads by area, and how much area the largest circle may take // is a house matter — a page of small multiples cannot spend what a full @@ -1432,6 +1483,62 @@ function liftBandAxis(spec: any, bandCh: 'x' | 'y', say: (p: string, m: string) } } +/** + * A grid puts a line at every tick of the axis it belongs to, and the tick at + * the end of a scale sits exactly where the *other* axis draws its domain. The + * two lines are then one line drawn twice, in two different inks — and the + * grid, being part of the panel, is drawn second: a 2.5px black spine comes + * out as a pale dashed rule in grid ink. + * + * Z-order is not the answer here. Vega-Lite hoists a shared axis out of a + * faceted plot's panels, so the panel's own grid is always laid down after it + * however the axis is ranked, and drawing a second spine to cover the damage + * would be inventing geometry. The honest fix is that the redundant line + * should not be there: the spine already states that edge, so the grid gives + * up its outermost line. Vega-Lite's conditional axis properties can say this + * exactly — `datum.index` is the tick's position along the axis as a fraction, + * so the line the domain lies on is the one at 0 or at 1. + */ +function dropGridUnderSpine(spec: any, config: any, say: (p: string, m: string) => void): void { + const drawsGrid = (cfg: any): boolean => + !!cfg?.grid && (cfg.gridWidth ?? 1) > 0 && !!cfg.gridColor && cfg.gridColor !== 'transparent'; + const drawsDomain = (cfg: any): boolean => + cfg?.domain !== false && !!cfg?.domainColor && cfg.domainColor !== 'transparent' + && (cfg.domainWidth ?? 1) > 0; + + let said = false; + for (const gridCh of ['x', 'y'] as const) { + const spineCh = gridCh === 'x' ? 'y' : 'x'; + const gridCfg = config[gridCh === 'x' ? 'axisX' : 'axisY']; + const spineCfg = config[spineCh === 'x' ? 'axisX' : 'axisY']; + if (!drawsGrid(gridCfg) || !drawsDomain(spineCfg)) continue; + // `index` runs from the start of the range to its end. A y range runs + // top-to-bottom, so its 0 is the bottom of the plot, where an x axis + // sits by default; an x range runs left-to-right, so its 0 is the left, + // where a y axis sits. + const far = spineCh === 'x' + ? (spineCfg.orient === 'top' ? 1 : 0) + : (spineCfg.orient === 'right' ? 1 : 0); + walk(spec, (node) => { + const enc = node.encoding?.[gridCh]; + if (!enc?.field || enc.axis === null) return; + if (enc.axis?.gridColor && typeof enc.axis.gridColor === 'object') return; + enc.axis = { + ...(enc.axis ?? {}), + gridColor: { + condition: { test: `datum.index === ${far}`, value: 'transparent' }, + value: gridCfg.gridColor, + }, + }; + if (!said) { + say(`axes.${gridCh}.grid`, + `the ${gridCh} grid drops its outermost line — the ${spineCh} axis already rules that edge, and a grid line over a spine repaints it in grid ink`); + said = true; + } + }); + } +} + /** * Vega-Lite's nested `line.point` mark does not inherit a literal colour from * its parent line, so a single-series themed line can become one colour with @@ -1572,24 +1679,53 @@ function applyTileGap( */ function applyConnectors(spec: any, d: DesignDecisions, say: (p: string, m: string) => void): void { const c = d.marks.connector; - if (!c?.show) return; + if (!c) return; + // A house that never styles connectors still gets its bridges corrected. + // A bridge joins two marks belonging to *different* series, so whichever + // series ink it inherits by default belongs to one of its two ends — it + // misattributes the span to one endpoint and, worse, makes that endpoint's + // dot indistinguishable from the line leaving it. A stem and a lead have + // no such ambiguity (a stem falls to the baseline within one series; a + // lead runs between two bars at one level), so where the house is silent + // they keep whatever the series pass gave them. + const bridgeOnly = !c.show; const said = new Set(); let sawStem = false; - const paint = (node: any, role: 'stem' | 'bridge' | 'lead'): void => { - if (role === 'stem') sawStem = true; + const paint = (node: any, role: 'stem' | 'bridge' | 'lead', enc: any): void => { + if (bridgeOnly && role !== 'bridge') return; + // A mark whose colour is encoded takes its ink from the data — a + // candlestick's wick is a rule that spans low to high but is coloured + // by whether the session closed up, whether that is said with a field + // or with a test and two values. Vega-Lite would discard a mark-level + // colour set beneath it. Structure has nothing to add to a line that + // is already saying something. + if ((node.encoding?.color ?? enc?.color) !== undefined) return; const mark = normalizeMark(node.mark); + // A mark that names its own stroke has already been given its ink by + // whoever drew it — a radar's spokes and rings are `rule`s that reach + // from centre to rim and so read as spans, but they are the chart's + // furniture, not a connector between two readings. Vega-Lite drops a + // `color` set alongside an explicit `stroke` anyway; skipping keeps + // that from being said twice and warned about once. + if (mark.stroke !== undefined) return; + if (role === 'stem') sawStem = true; if (c.color) mark.color = c.color; - mark.strokeWidth = role === 'bridge' ? c.spanWidth : c.width; - if (c.dash) mark.strokeDash = c.dash; + // Correcting an undeclared bridge is a correction of colour, not of + // weight: the house said nothing about how heavy its spans should be, + // and the series pass already sized this line like every other line. + if (!bridgeOnly) mark.strokeWidth = role === 'bridge' ? c.spanWidth : c.width; + if (c.dash && !bridgeOnly) mark.strokeDash = c.dash; node.mark = mark; if (said.has(role)) return; said.add(role); - const why = { - bridge: `the bridge is drawn at ${c.spanWidth}px in structural ink — the distance it spans is the reading, so it carries a mark's weight and none of a series' colour`, - stem: `the stem is drawn at ${c.width}px in structural ink — it leads the eye to the axis and states nothing the dot's position has not`, - lead: `the lead line is drawn at ${c.width}px in structural ink — it runs across the categories at one level, and the two mark ends it touches already state that level`, - }[role]; + const why = bridgeOnly + ? `the bridge is drawn in structural ink — it joins two series' marks, so any series colour it took would credit the span to one of its two ends` + : { + bridge: `the bridge is drawn at ${c.spanWidth}px in structural ink — the distance it spans is the reading, so it carries a mark's weight and none of a series' colour`, + stem: `the stem is drawn at ${c.width}px in structural ink — it leads the eye to the axis and states nothing the dot's position has not`, + lead: `the lead line is drawn at ${c.width}px in structural ink — it runs across the categories at one level, and the two mark ends it touches already state that level`, + }[role]; say('marks.connector', why); }; @@ -1609,7 +1745,7 @@ function applyConnectors(spec: any, d: DesignDecisions, say: (p: string, m: stri if (end) { const start = along === 'x' ? (own.x ?? enc.x) : (own.y ?? enc.y); const acrossCategories = start?.type === 'nominal' || start?.type === 'ordinal'; - paint(node, !end.field ? 'stem' : acrossCategories ? 'lead' : 'bridge'); + paint(node, !end.field ? 'stem' : acrossCategories ? 'lead' : 'bridge', enc); } } else if (type === 'line') { // A line grouped by the field the categorical axis already @@ -1619,7 +1755,7 @@ function applyConnectors(spec: any, d: DesignDecisions, say: (p: string, m: stri const band = (['x', 'y'] as const) .map((ch) => enc[ch]) .find((e: any) => e?.field && (e.type === 'nominal' || e.type === 'ordinal')); - if (key && band?.field === key) paint(node, 'bridge'); + if (key && band?.field === key) paint(node, 'bridge', enc); } } for (const k of ['layer', 'vconcat', 'hconcat', 'concat']) { @@ -1862,6 +1998,77 @@ function dotOuterDiameter(size: number, strokeWidth: number): number { return 2 * Math.sqrt(size / Math.PI) + strokeWidth; } +/** + * A dot has a radius; a scale fitted to the data does not know that. + * + * Vega-Lite sizes a continuous scale to the extremes of the field, which puts + * the *centre* of the outermost dot exactly on the edge of the plot and throws + * away the half of it that falls outside — the lowest value in a dumbbell ends + * up as a half-moon glued to the y axis, sitting on its own row label. A bar + * never has this trouble because its end *is* the edge; only a mark with ink + * either side of its position does. + * + * So the range is opened by the dot's own radius. `scale.padding` is measured + * in pixels, which is the unit the problem is in: it costs the same sliver of + * plot whatever the data happens to span, and it moves no value. + * + * Held back wherever something on the chart is measured *from* the edge — a + * bar, an area, a lollipop's stem — because there the edge is a base, and a + * base that has been nudged off zero is worse than a clipped dot. + */ +function padScaleForDots(spec: any, size: number, strokeWidth: number, say: (p: string, m: string) => void): void { + const radius = Math.ceil(dotOuterDiameter(size, strokeWidth) / 2); + if (radius <= 0) return; + // A dumbbell keeps its position channels on the parent and gives the dot + // layer only its colour, so the scale to open is not always written on the + // node that carries the mark. Walk down remembering where each channel was + // last declared, and pad that. + let anchored = false; + const dots: Record[] = []; + const visit = (node: any, declared: Record): void => { + if (!node || typeof node !== 'object') return; + const next = { ...declared }; + for (const ch of ['x', 'y'] as const) { + if (node.encoding?.[ch]?.field) next[ch] = node.encoding[ch]; + } + if (node.mark && !isLiteralMark(node)) { + const type = markTypeOf(node.mark); + if (type === 'bar' || type === 'area' || type === 'rect' || type === 'rule' || type === 'arc') anchored = true; + else if (type && POINT_MARKS.has(type)) dots.push(next); + } + for (const k of ['layer', 'vconcat', 'hconcat', 'concat']) { + if (Array.isArray(node[k])) node[k].forEach((child: any) => visit(child, next)); + } + if (node.spec) visit(node.spec, next); + if (node.facet?.spec) visit(node.facet.spec, next); + }; + visit(spec, {}); + if (anchored || !dots.length) return; + + let padded = false; + for (const declared of dots) { + for (const ch of ['x', 'y'] as const) { + const enc = declared[ch]; + if (!enc?.field) continue; + if (enc.type !== 'quantitative' && enc.type !== 'temporal') continue; + // A scale pinned to zero, or to a domain the caller chose, is + // saying where its ends are. Padding would move them. + if (enc.scale?.zero === true || enc.scale?.domain || enc.scale?.padding != null) continue; + // `nice` rounds the domain outward to the next whole tick, and it + // does that *after* the padding is folded in — so a 4px gap turns + // into a whole extra interval of empty plot and an axis labelled + // past where the data goes. The padding is the breathing room; the + // rounding on top of it is not wanted. + enc.scale = { ...(enc.scale ?? {}), padding: radius, nice: enc.scale?.nice ?? false }; + padded = true; + } + } + if (padded) { + say('marks.point.size', + `the plot is opened by ${radius}px at each end — a dot's own radius — so the outermost reading sits inside the axes instead of half outside them`); + } +} + /** * The most of a bar's own thickness a rounded end may eat. * @@ -1936,6 +2143,43 @@ function bandStep(spec: any, node: any, enc: any, channel: 'x' | 'y', table: any return size / count; } +/** + * A group divider — the dashed rule a grouped box or violin plot draws between + * one department and the next — is written by the template as `bandPosition: 1`, + * the end of the band. That is the middle of the gap only while the gap is + * nothing: `bandPosition` is measured in band *widths*, so once a house asks for + * `paddingInner` the band stops short of the step and the divider lands hard + * against the right shoulder of the group on its left, reading as that group's + * edge rather than as a boundary between two. + * + * The gap runs from the band's end to the next band's start and is + * `step - width` wide; in band widths that is `paddingInner / (1 - paddingInner)`. + * Half of it puts the rule in the middle, where it belongs — equidistant from + * the two groups it separates, which is the whole claim a divider makes. + */ +function centerBandDividers(spec: any, paddedFields: Set, paddingInner: number, say: (p: string, m: string) => void): void { + if (paddingInner <= 0 || paddedFields.size === 0) return; + const centered = 1 + paddingInner / (2 * (1 - paddingInner)); + let said = false; + walk(spec, (node: any) => { + if (markTypeOf(node.mark) !== 'rule') return; + for (const channel of ['x', 'y'] as const) { + const e = node.encoding?.[channel]; + // A rule with a second end on the same axis spans a range and is + // anchored at both ends on purpose — a waterfall's connector reaches + // from one bar's edge to the next and must keep touching them. + if (!e || node.encoding?.[`${channel}2`]) continue; + if (e.bandPosition !== 1 || !paddedFields.has(e.field)) continue; + e.bandPosition = centered; + if (!said) { + say('marks.bandFraction', + 'group dividers move to the middle of the gap the house opened between bands'); + said = true; + } + } + }); +} + /** * A bar sitting on a continuous positional axis the layout has banded (a year, * a date): one axis carries a temporal or quantitative field that is not @@ -2359,6 +2603,66 @@ function applySeriesInk(spec: any, d: DesignDecisions, table: any[], say: (p: st // roles, not series: they keep their hues and only move to the same // distance from the surface they now sit on. Left alone, a white card // stays a white island on a dark canvas. + // A label the template wrote itself — a radar's axis names, drawn as text + // marks because the plot has no axis to hang them on — is chrome the house + // owns. It is positioned by fields, so the furniture pass below does not + // see it, and its literal grey was picked against flint's white: on a dark + // canvas it sinks into the surface. The text is a constant, so nothing here + // is a reading; it takes the house's secondary ink like any other label. + let saidLabels = false; + walk(spec, (node) => { + if (markTypeOf(node.mark) !== 'text' || node.__themeSynthetic || node.__themeRole) return; + if (node.encoding?.text?.value === undefined) return; + const mark = normalizeMark(node.mark); + let changed = false; + for (const key of ['fill', 'color'] as const) { + if (typeof mark[key] === 'string' && mark[key] !== d.text.secondary) { + mark[key] = d.text.secondary; + changed = true; + } + } + if (!changed) return; + node.mark = mark; + if (!saidLabels) { + say('type.axisLabel', + 'the template wrote its own labels as text marks — those are chrome and take the house label ink, not the literal grey they were drawn in'); + saidLabels = true; + } + }); + + // Vega-Lite paints a box plot's whiskers and caps from the mark's colour, + // but only when the mark states one: with the colour *encoded*, the box + // takes the series ink and the rule falls back to a literal black. On a + // dark canvas the extremes then disappear entirely. The whisker is + // structure holding a reading, so it takes the house's text ink — which on + // a light house is the black it already was. + let saidWhisker = false; + walk(spec, (node) => { + if (markTypeOf(node.mark) !== 'boxplot') return; + const encoded = node.encoding?.color?.field ?? node.encoding?.fill?.field; + if (!encoded) return; + const mark = normalizeMark(node.mark); + if (mark.color != null) return; + let changed = false; + for (const part of ['rule', 'ticks'] as const) { + const own = mark[part]; + // `ticks` is off unless the mark asks for it, and an object *is* + // asking — so only an existing tick gets recoloured. A rule is + // always drawn, so it can be given one. + if (part === 'ticks' && !(own && typeof own === 'object')) continue; + if (own && typeof own === 'object' && own.color != null) continue; + mark[part] = { ...(typeof own === 'object' ? own : {}), color: d.text.primary }; + changed = true; + } + if (!changed) return; + node.mark = mark; + if (!saidWhisker) { + say('ink.series', + 'the box takes its ink from the colour channel, which leaves the whiskers literal black — they take the text ink so the extremes stay legible on any surface'); + saidWhisker = true; + } + }); + walk(spec, (node) => { if (!markTypeOf(node.mark) || node.__themeSynthetic || !isLiteralMark(node)) return; if (node.__themeRole) return; diff --git a/packages/flint-js/tests/theme-presets.test.ts b/packages/flint-js/tests/theme-presets.test.ts index ac71d21b..403ba270 100644 --- a/packages/flint-js/tests/theme-presets.test.ts +++ b/packages/flint-js/tests/theme-presets.test.ts @@ -1008,3 +1008,322 @@ describe('KPI card progress bar takes house ink', () => { }); } }); + +/** + * A dumbbell's connector is not a third series. It joins one row's two dots, + * so whichever dot's ink it borrows it credits that dot with the whole span — + * and, worse, the line leaving that dot becomes indistinguishable from the dot + * itself. The bridge has to be structural: quiet, and its own colour. + */ +describe('ranged dot connector', () => { + const ROWS = ['USA', 'China', 'Japan', 'Germany'].flatMap((c, i) => [ + { Country: c, Value: 30 + i * 5, Metric: 'Min' }, + { Country: c, Value: 70 + i * 5, Metric: 'Max' }, + ]); + + function dumbbell(themeId: string): any { + return assembleVegaLite({ + data: { values: ROWS }, + semantic_types: { Country: 'Country', Value: 'Quantity', Metric: 'Category' }, + chart_spec: { + chartType: 'Ranged Dot Plot', + encodings: { x: 'Value', y: 'Country', color: 'Metric' }, + baseSize: { width: 480, height: 320 }, + }, + theme_spec: THEME_PRESETS[themeId].spec, + } as any) as any; + } + + // A house may wrap the plot (datawrapper puts its key in a `vconcat`), so + // neither the bridge nor the dots are reliably a top-level layer. + function collect(spec: any): { bridge?: any; range: string[] } { + let bridge: any; + const range: string[] = []; + const visit = (n: any): void => { + if (!n || typeof n !== 'object') return; + if (Array.isArray(n)) { n.forEach(visit); return; } + if (n.mark && markTypeOf(n.mark) === 'line' && !bridge) bridge = n; + const r = n.encoding?.color?.scale?.range; + if (Array.isArray(r)) range.push(...r); + for (const k of Object.keys(n)) if (k !== 'data') visit(n[k]); + }; + visit(spec); + return { bridge, range }; + } + + for (const id of Object.keys(THEME_PRESETS)) { + it(`${id}: the bridge carries none of the dots' colour`, () => { + const { bridge, range } = collect(dumbbell(id)); + expect(bridge).toBeTruthy(); + expect(range.length).toBeGreaterThan(1); + + const ink = bridge.mark?.color; + expect(ink).toBeTruthy(); + for (const series of range) { + expect(String(ink).toLowerCase()).not.toBe(String(series).toLowerCase()); + } + }); + } +}); + +/** + * Three rules about where a chart's walls are, and what may paint over them. + */ +describe('plot edges', () => { + const STRIP = ['Control', 'Treatment A', 'Treatment B'].flatMap((g) => + [38, 55, 72, 88, 100].map((v) => ({ Group: g, Score: v + g.length }))); + + function strip(themeId: string): any { + return assembleVegaLite({ + data: { values: STRIP }, + semantic_types: { Group: 'Category', Score: 'Quantity' }, + chart_spec: { + chartType: 'Strip Plot', + encodings: { x: 'Group', y: 'Score' }, + baseSize: { width: 480, height: 320 }, + }, + theme_spec: THEME_PRESETS[themeId].spec, + } as any) as any; + } + + // A house that draws no domain at all is not making a point about zero. + const RULES_A_DOMAIN = Object.keys(THEME_PRESETS).filter((id) => { + const c = strip(id).config ?? {}; + return c.axisY?.domainColor && c.axisY.domainColor !== 'transparent'; + }); + + it('some house draws a domain, so the rule is actually under test', () => { + expect(RULES_A_DOMAIN.length).toBeGreaterThan(0); + }); + + for (const id of RULES_A_DOMAIN) { + it(`${id}: a strip plot keeps the wall under its categories`, () => { + // Nothing on a strip plot is measured from the bottom of the plot, + // so the line there is a wall, not a false zero. + expect(strip(id).config?.axisX?.domain).not.toBe(false); + }); + } + + for (const id of Object.keys(THEME_PRESETS)) { + it(`${id}: dots are given room to sit inside the axes`, () => { + const spec = strip(id); + const pads: number[] = []; + const visit = (n: any): void => { + if (!n || typeof n !== 'object') return; + if (Array.isArray(n)) { n.forEach(visit); return; } + const p = n.encoding?.y?.scale?.padding; + if (typeof p === 'number') pads.push(p); + for (const k of Object.keys(n)) if (k !== 'data') visit(n[k]); + }; + visit(spec); + expect(pads.length).toBeGreaterThan(0); + // Room enough for the radius of the dot the house draws. + expect(Math.min(...pads)).toBeGreaterThan(0); + }); + + it(`${id}: a grid never repaints the spine it lands on`, () => { + const spec = strip(id); + const cfg = spec.config ?? {}; + const gridded = (['x', 'y'] as const).filter((ch) => { + const c = cfg[ch === 'x' ? 'axisX' : 'axisY']; + return c?.grid && c.gridColor && c.gridColor !== 'transparent'; + }); + for (const ch of gridded) { + const other = cfg[ch === 'x' ? 'axisY' : 'axisX']; + const spine = other?.domain !== false && other?.domainColor + && other.domainColor !== 'transparent'; + if (!spine) continue; + let conditional = false; + const visit = (n: any): void => { + if (!n || typeof n !== 'object') return; + if (Array.isArray(n)) { n.forEach(visit); return; } + const g = n.encoding?.[ch]?.axis?.gridColor; + if (g && typeof g === 'object' && g.condition) conditional = true; + for (const k of Object.keys(n)) if (k !== 'data') visit(n[k]); + }; + visit(spec); + expect(conditional).toBe(true); + } + }); + } + + for (const id of RULES_A_DOMAIN) { + it(`${id}: a dumbbell keeps the wall beside its rows`, () => { + // Same narrowing, the other way round: the dumbbell's value scale + // floats and its rows are bands, but the dots are read against the + // ticks, not measured from the left-hand wall. + const spec: any = assembleVegaLite({ + data: { + values: ['USA', 'China', 'Japan'].flatMap((c, i) => [ + { Country: c, Value: 30 + i * 5, Metric: 'Min' }, + { Country: c, Value: 70 + i * 5, Metric: 'Max' }, + ]), + }, + semantic_types: { Country: 'Country', Value: 'Quantity', Metric: 'Category' }, + chart_spec: { + chartType: 'Ranged Dot Plot', + encodings: { x: 'Value', y: 'Country', color: 'Metric' }, + baseSize: { width: 480, height: 320 }, + }, + theme_spec: THEME_PRESETS[id].spec, + } as any); + expect(spec.config?.axisY?.domain).not.toBe(false); + }); + } +}); + +describe('group dividers', () => { + // A dashed rule between one group of boxes and the next is written as + // `bandPosition: 1` — the end of the band, which is the middle of the gap + // only while there is no gap. Every house opens one, so the divider has to + // move with it or it reads as the left-hand group's own right edge. + // Sparse on purpose: dividers are drawn only when the lanes are packed + // locally, which is what a house does when not every group has every level. + const rows = ['Eng', 'Sales', 'HR', 'Ops', 'Legal'].flatMap((dept, d) => + ['L1', 'L2', 'L3', 'L4', 'L5'].slice(d % 3, (d % 3) + 2).flatMap((level) => + [90, 100, 110, 120].map((v, i) => ({ Department: dept, Level: level, Comp: v * 1000 + i })), + ), + ); + let anyDrawn = false; + + for (const id of Object.keys(THEME_PRESETS)) { + it(`${id}: centres group dividers in the gap between bands`, () => { + const spec: any = assembleVegaLite({ + data: { values: rows }, + semantic_types: { Department: 'Category', Level: 'Category', Comp: 'Currency' }, + chart_spec: { + chartType: 'Boxplot', + encodings: { x: 'Department', y: 'Comp', color: 'Level' }, + baseSize: { width: 480, height: 320 }, + }, + theme_spec: THEME_PRESETS[id].spec, + } as any); + + let padding: number | undefined; + const positions: number[] = []; + const collect = (node: any): void => { + if (!node || typeof node !== 'object') return; + for (const channel of ['x', 'y'] as const) { + const e = node.encoding?.[channel]; + if (e?.field !== 'Department') continue; + if (typeof e.scale?.paddingInner === 'number') padding = e.scale.paddingInner; + if (typeof e.bandPosition === 'number' && !node.encoding[`${channel}2`]) { + positions.push(e.bandPosition); + } + } + for (const key of ['layer', 'vconcat', 'hconcat', 'concat']) { + if (Array.isArray(node[key])) node[key].forEach(collect); + } + if (node.spec) collect(node.spec); + if (node.facet?.spec) collect(node.facet.spec); + }; + collect(spec); + + if (positions.length === 0) return; // this house's layout drew no dividers + anyDrawn = true; + const p = padding ?? 0; + // Band start + this many band widths lands halfway across the gap. + const expected = 1 + p / (2 * (1 - p)); + for (const got of positions) expect(got).toBeCloseTo(expected, 6); + }); + } + + it('draws dividers at all', () => { + expect(anyDrawn).toBe(true); + }); +}); + +describe('legibility on the house surface', () => { + const luminance = (hex: string): number => { + const h = hex.replace('#', ''); + const full = h.length === 3 ? h.split('').map((c) => c + c).join('') : h; + const [r, g, b] = [0, 2, 4].map((i) => parseInt(full.slice(i, i + 2), 16) / 255) + .map((v) => (v <= 0.03928 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4)); + return 0.2126 * r + 0.7152 * g + 0.0722 * b; + }; + const contrast = (a: string, b: string): number => { + const [hi, lo] = [luminance(a), luminance(b)].sort((x, y) => y - x); + return (hi + 0.05) / (lo + 0.05); + }; + const surfaceOf = (spec: any): string => + spec.config?.view?.fill ?? spec.config?.background ?? spec.background ?? '#ffffff'; + + const boxRows = ['Eng', 'Sales', 'HR'].flatMap((dept) => + ['L1', 'L2'].flatMap((level) => + [90, 100, 110, 120].map((v, i) => ({ Department: dept, Level: level, Comp: v * 1000 + i })), + ), + ); + + for (const id of Object.keys(THEME_PRESETS)) { + it(`${id}: box plot whiskers stay legible when the box takes a colour channel`, () => { + const spec: any = assembleVegaLite({ + data: { values: boxRows }, + semantic_types: { Department: 'Category', Level: 'Category', Comp: 'Currency' }, + chart_spec: { + chartType: 'Boxplot', + encodings: { x: 'Department', y: 'Comp', color: 'Level' }, + baseSize: { width: 480, height: 320 }, + }, + theme_spec: THEME_PRESETS[id].spec, + } as any); + + const inks: string[] = []; + const collect = (node: any): void => { + if (!node || typeof node !== 'object') return; + const mark = typeof node.mark === 'string' ? { type: node.mark } : node.mark; + if (mark?.type === 'boxplot' && (node.encoding?.color?.field || node.encoding?.fill?.field)) { + inks.push(mark.rule?.color ?? mark.color ?? '#000000'); + } + for (const key of ['layer', 'vconcat', 'hconcat', 'concat']) { + if (Array.isArray(node[key])) node[key].forEach(collect); + } + if (node.spec) collect(node.spec); + if (node.facet?.spec) collect(node.facet.spec); + }; + collect(spec); + + expect(inks.length).toBeGreaterThan(0); + const surface = surfaceOf(spec); + for (const ink of inks) expect(contrast(ink, surface)).toBeGreaterThan(3); + }); + + it(`${id}: radar axis labels stay legible on the house surface`, () => { + const spec: any = assembleVegaLite({ + data: { + values: ['A', 'B'].flatMap((team) => + ['Speed', 'Attack', 'Tactics', 'Stamina'].map((axis, i) => ({ + Team: team, Measure: axis, Score: 40 + i * 12, + })), + ), + }, + semantic_types: { Team: 'Category', Measure: 'Category', Score: 'Quantity' }, + chart_spec: { + chartType: 'Radar Chart', + encodings: { x: 'Measure', y: 'Score', color: 'Team' }, + baseSize: { width: 480, height: 360 }, + }, + theme_spec: THEME_PRESETS[id].spec, + } as any); + + const inks: string[] = []; + const collect = (node: any): void => { + if (!node || typeof node !== 'object') return; + const mark = typeof node.mark === 'string' ? { type: node.mark } : node.mark; + if (mark?.type === 'text' && node.encoding?.text?.value !== undefined) { + const ink = mark.fill ?? mark.color; + if (typeof ink === 'string') inks.push(ink); + } + for (const key of ['layer', 'vconcat', 'hconcat', 'concat']) { + if (Array.isArray(node[key])) node[key].forEach(collect); + } + if (node.spec) collect(node.spec); + if (node.facet?.spec) collect(node.facet.spec); + }; + collect(spec); + + expect(inks.length).toBeGreaterThan(0); + const surface = surfaceOf(spec); + for (const ink of inks) expect(contrast(ink, surface)).toBeGreaterThan(3); + }); + } +}); From 3a9facd3df7898545415738815398ae72225d0c5 Mon Sep 17 00:00:00 2001 From: Chenglong Wang Date: Mon, 3 Aug 2026 00:16:58 -0700 Subject: [PATCH 082/164] theme: a house switch in the gallery and the MCP app MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The themes had no way in. Both option bars now open with a house switch, left of the chart-type switch, because a theme is the outermost decision on a chart — it settles the surface, the ink and the type that everything the other controls touch is then drawn in. Every preset ships a 16px icon, because a picker has to say what a house looks like before the reader has seen a chart in it, and a word cannot: "Economist" and "Datawrapper" are both blue bars on white to anyone who has not met them. Each icon is a tiny chart drawn in the house's own decisions — its canvas, the first three of its categorical set, the weight of its rules — rather than an invented glyph, so nothing in one is a colour its charts will not use. They are authored in a single file because an icon set is only legible as a set: at this size there is room for exactly one difference each, so each house gets one and only one. NYT leads with a black headline bar, the Economist flies its red flag, Nature draws a bare black L with no grid, McKinsey lays its bars horizontally, Datawrapper rules its field, Power BI is simply dark, Swiss is heavy black structure on warm paper, and cartoon has rounded tops. Flint's own defaults get one too, so "no house" is a visible choice rather than an empty slot. The icons stay out of `listThemePresets` and out of the `list_themes` tool result: that is what an agent reads to choose a house, and a picture it cannot see is context spent for nothing. A picker reads them off `THEME_PRESETS` instead. Only Vega-Lite reads `theme_spec`, so in the gallery the switch is absent on the other backends rather than inert — a control that does nothing reads as a bug in the theme, which is the one thing a theme must never look like. In the MCP app `withTheme` sets the key at the top of the input rather than inside `chart_spec`, since the same house applies whatever the chart turns out to be, and reset now notices it. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 175e0e03-5dda-4a02-aa14-c181660b9c99 --- packages/flint-js/src/core/index.ts | 1 + packages/flint-js/src/core/theme/index.ts | 2 +- packages/flint-js/src/core/theme/presets.ts | 15 +- .../src/core/theme/presets/cartoon.ts | 2 + .../src/core/theme/presets/datawrapper.ts | 2 + .../src/core/theme/presets/economist.ts | 2 + .../flint-js/src/core/theme/presets/icons.ts | 159 ++++++++++++++++++ .../src/core/theme/presets/mckinsey.ts | 2 + .../flint-js/src/core/theme/presets/nature.ts | 2 + .../flint-js/src/core/theme/presets/nyt.ts | 2 + .../src/core/theme/presets/powerbi-light.ts | 2 + .../src/core/theme/presets/powerbi.ts | 2 + .../flint-js/src/core/theme/presets/swiss.ts | 2 + packages/flint-js/src/core/theme/types.ts | 13 ++ packages/flint-js/tests/theme-presets.test.ts | 64 ++++++- packages/flint-mcp/src/tools/list.ts | 4 +- packages/flint-mcp/ui/src/FlintApp.tsx | 101 ++++++++++- packages/flint-mcp/ui/src/options.ts | 13 ++ site/src/components/ChartCodeModal.tsx | 17 +- site/src/components/GalleryOptionsBar.tsx | 152 ++++++++++++++++- site/src/components/WallChart.tsx | 17 +- site/src/routes/Landing.tsx | 25 ++- 22 files changed, 581 insertions(+), 20 deletions(-) create mode 100644 packages/flint-js/src/core/theme/presets/icons.ts diff --git a/packages/flint-js/src/core/index.ts b/packages/flint-js/src/core/index.ts index 772d32ef..3fed2acd 100644 --- a/packages/flint-js/src/core/index.ts +++ b/packages/flint-js/src/core/index.ts @@ -205,6 +205,7 @@ export { type GroundingContext, groundTheme, THEME_PRESETS, + DEFAULT_THEME_ICON, listThemePresets, resolveThemeSpec, } from './theme'; diff --git a/packages/flint-js/src/core/theme/index.ts b/packages/flint-js/src/core/theme/index.ts index ed38928c..7b9b0b07 100644 --- a/packages/flint-js/src/core/theme/index.ts +++ b/packages/flint-js/src/core/theme/index.ts @@ -5,4 +5,4 @@ export * from './types.js'; export * from './presence.js'; export { groundTheme } from './ground.js'; export type { GroundingContext } from './ground.js'; -export { THEME_PRESETS, listThemePresets, resolveThemeSpec } from './presets.js'; +export { THEME_PRESETS, DEFAULT_THEME_ICON, listThemePresets, resolveThemeSpec } from './presets.js'; diff --git a/packages/flint-js/src/core/theme/presets.ts b/packages/flint-js/src/core/theme/presets.ts index b38d8641..b0a5b8c6 100644 --- a/packages/flint-js/src/core/theme/presets.ts +++ b/packages/flint-js/src/core/theme/presets.ts @@ -10,6 +10,7 @@ */ import type { ThemePreset, ThemeSpec } from './types'; +import { FLINT_ICON } from './presets/icons'; import { nyt } from './presets/nyt'; import { economist } from './presets/economist'; import { nature } from './presets/nature'; @@ -32,7 +33,19 @@ export const THEME_PRESETS: Record = { cartoon, }; -/** The catalogue, without the specs — enough to choose by. */ +/** + * The icon for "no house" — flint's own defaults, so a picker can offer + * *not* theming as a visible choice rather than an empty slot. + */ +export const DEFAULT_THEME_ICON = FLINT_ICON; + +/** + * The catalogue, without the specs — enough to choose by. + * + * Without the icons either: this is what an agent reads to pick a house, and a + * picture it cannot see costs it context it could have spent on the chart. A + * picker that wants icons reads them off {@link THEME_PRESETS}. + */ export function listThemePresets(): Array> { return Object.values(THEME_PRESETS).map(({ id, label, description }) => ({ id, label, description })); } diff --git a/packages/flint-js/src/core/theme/presets/cartoon.ts b/packages/flint-js/src/core/theme/presets/cartoon.ts index ef8173ae..d6791371 100644 --- a/packages/flint-js/src/core/theme/presets/cartoon.ts +++ b/packages/flint-js/src/core/theme/presets/cartoon.ts @@ -2,6 +2,7 @@ // Licensed under the MIT License. import type { ThemePreset } from '../types'; +import { CARTOON_ICON } from './icons'; /** * Cartoon — a playful, friendly house in the spirit of xkcd and modern @@ -32,6 +33,7 @@ export const cartoon: ThemePreset = { '- Annotate the measure with `unit` in `semantic_types`.', '- Colour is a bright crayon set; the key tells 6 series apart.', ].join('\n'), + icon: CARTOON_ICON, spec: { id: 'cartoon', label: 'Cartoon', diff --git a/packages/flint-js/src/core/theme/presets/datawrapper.ts b/packages/flint-js/src/core/theme/presets/datawrapper.ts index e283f975..00d4d497 100644 --- a/packages/flint-js/src/core/theme/presets/datawrapper.ts +++ b/packages/flint-js/src/core/theme/presets/datawrapper.ts @@ -2,6 +2,7 @@ // Licensed under the MIT License. import type { ThemePreset } from '../types'; +import { DATAWRAPPER_ICON } from './icons'; /** * Datawrapper. @@ -17,6 +18,7 @@ export const datawrapper: ThemePreset = { "- Sized for a narrow column, so it reads tall rather than wide.", "- Colour can tell 5 categories apart.", ].join('\n'), + icon: DATAWRAPPER_ICON, spec: { "id": "datawrapper", "label": "Datawrapper", diff --git a/packages/flint-js/src/core/theme/presets/economist.ts b/packages/flint-js/src/core/theme/presets/economist.ts index 769041b0..19ecbee2 100644 --- a/packages/flint-js/src/core/theme/presets/economist.ts +++ b/packages/flint-js/src/core/theme/presets/economist.ts @@ -2,6 +2,7 @@ // Licensed under the MIT License. import type { ThemePreset } from '../types'; +import { ECONOMIST_ICON } from './icons'; /** * The Economist. @@ -17,6 +18,7 @@ export const economist: ThemePreset = { "- Annotate each measure with `unit` in `semantic_types`.", "- The key holds 3 colours.", ].join('\n'), + icon: ECONOMIST_ICON, spec: { "id": "economist", "label": "The Economist", diff --git a/packages/flint-js/src/core/theme/presets/icons.ts b/packages/flint-js/src/core/theme/presets/icons.ts new file mode 100644 index 00000000..40f788f5 --- /dev/null +++ b/packages/flint-js/src/core/theme/presets/icons.ts @@ -0,0 +1,159 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** + * The houses at 16 pixels. + * + * A theme picker has to say what a house *looks like* before the reader has + * seen a chart in it, and a word cannot: "Economist" and "Datawrapper" are both + * blue bars on white to anyone who has not met them. So each icon is a tiny + * chart drawn in the house's own decisions — its canvas, the first three of its + * categorical set, the weight of its rules — rather than an invented glyph. + * Nothing here is a colour the house does not already use. + * + * They are authored in one file on purpose. An icon set is only legible as a + * set: what makes each one recognisable is the thing it does that its + * neighbours do not, and that can only be judged side by side. At this size + * there is room for exactly one such difference each, so every house gets one + * and only one: + * + * flint no house at all — one flat grey trio, no signature + * nyt a black headline bar over the plot, the way its charts lead + * economist the red flag, top left + * nature a bare black L of axis and thin journal bars, no grid + * mckinsey bars laid horizontally, the deck-chart posture + * datawrapper horizontal grid ruling read through the bars + * powerbi the dark canvas + * powerbi-light the same bright series on white, with a faint grid + * swiss heavy structural black rules on warm paper + * cartoon rounded bar tops and a thick soft outline + * + * Each is a complete SVG document so a caller can put it straight in an `` + * or inline it. 16×16 with a half-pixel inset frame: the frame is what lets a + * white house read as a *tile* rather than as marks floating on the toolbar. + */ + +/** Shared geometry, so the set lines up when the icons sit next to each other. */ +const BASELINE = 12.5; + +function tile(canvas: string, frame: string, body: string, radius = 2): string { + return [ + '', + ``, + body, + '', + ].join(''); +} + +/** Three upright bars on the shared baseline, at the heights the set uses. */ +function bars( + colors: [string, string, string], + heights: [number, number, number], + width = 2.6, + radius = 0, +): string { + const x = [3, 6.7, 10.4]; + return colors + .map((fill, i) => { + const h = heights[i]; + const rx = radius ? ` rx="${radius}"` : ''; + return ``; + }) + .join(''); +} + +/** A horizontal rule — a baseline, a gridline, the pale ruling behind bars. */ +function rule(x1: number, y: number, x2: number, stroke: string, width = 1): string { + return ``; +} + +/** No house: flint's own defaults, stated plainly so "none" is a visible choice. */ +export const FLINT_ICON = tile( + '#ffffff', + '#dcdcdc', + bars(['#4c78a8', '#f58518', '#e45756'], [6.5, 8.5, 5]) + + rule(2.6, BASELINE, 13.4, '#bdbdbd'), +); + +/** A black headline bar, then the plot — the order an NYT chart is read in. */ +export const NYT_ICON = tile( + '#ffffff', + '#dcdcdc', + '' + + bars(['#2f6b9a', '#c2352b', '#4a8b6f'], [6, 7.6, 4.6]) + + rule(2.6, BASELINE, 13.4, '#121212'), +); + +/** The red flag in the corner, and the house blues under one pale rule. */ +export const ECONOMIST_ICON = tile( + '#ffffff', + '#dcdcdc', + '' + + bars(['#006ba2', '#3ebcd2', '#ebb434'], [6, 7.6, 4.6]) + + rule(2.6, BASELINE, 13.4, '#121317'), +); + +/** A bare black L and thin bars: a journal figure, no grid, no ornament. */ +export const NATURE_ICON = tile( + '#ffffff', + '#dcdcdc', + bars(['#0072b2', '#e69f00', '#009e73'], [6.2, 8, 4.6], 2) + + ``, +); + +/** Bars laid on their side against a single spine — the deck-chart posture. */ +export const MCKINSEY_ICON = tile( + '#ffffff', + '#dcdcdc', + '' + + '' + + '' + + '', +); + +/** Grid ruling read straight through the bars, the way its charts are gridded. */ +export const DATAWRAPPER_ICON = tile( + '#ffffff', + '#dcdcdc', + rule(2.6, 5.5, 13.4, '#b3b3b3') + + rule(2.6, 8, 13.4, '#b3b3b3') + + rule(2.6, 10.5, 13.4, '#b3b3b3') + + bars(['#18a1cd', '#e2a233', '#c04a4a'], [6.5, 8.5, 5], 2.2) + + rule(2.6, BASELINE, 13.4, '#333333'), +); + +/** The dark canvas, which is the whole point of the house. */ +export const POWERBI_ICON = tile( + '#1b1a19', + '#3b3a39', + rule(2.6, 7.6, 13.4, '#3b3a39') + + bars(['#118dff', '#12239e', '#e66c37'], [6.5, 8.5, 5]) + + rule(2.6, BASELINE, 13.4, '#3b3a39'), +); + +/** The same series, on white — the pair reads as one house in two surfaces. */ +export const POWERBI_LIGHT_ICON = tile( + '#ffffff', + '#d2d0ce', + rule(2.6, 7.6, 13.4, '#dcdcdc') + + bars(['#118dff', '#12239e', '#e66c37'], [6.5, 8.5, 5]) + + rule(2.6, BASELINE, 13.4, '#d2d0ce'), +); + +/** Warm paper, square marks, and axes drawn as structure rather than hinted. */ +export const SWISS_ICON = tile( + '#f4f1ea', + '#d9d5cc', + bars(['#e2231a', '#1a1a1a', '#0067a5'], [6.2, 8.2, 4.8]) + + ``, + 0, +); + +/** Rounded tops and a thick soft rule: the drawn-by-hand register. */ +export const CARTOON_ICON = tile( + '#fffdf5', + '#ece5d6', + bars(['#3aa9ff', '#ff5d5d', '#ffc23c'], [6.4, 8.4, 5], 2.8, 1.3) + + rule(2.6, BASELINE, 13.4, '#2e2b28', 1.7), + 3.5, +); diff --git a/packages/flint-js/src/core/theme/presets/mckinsey.ts b/packages/flint-js/src/core/theme/presets/mckinsey.ts index 4c56acec..299909d2 100644 --- a/packages/flint-js/src/core/theme/presets/mckinsey.ts +++ b/packages/flint-js/src/core/theme/presets/mckinsey.ts @@ -2,6 +2,7 @@ // Licensed under the MIT License. import type { ThemePreset } from '../types'; +import { MCKINSEY_ICON } from './icons'; /** * McKinsey. @@ -16,6 +17,7 @@ export const mckinsey: ThemePreset = { "- `title` states the takeaway; `subtitle` names the measure and unit.", "- Colour can tell 5 categories apart.", ].join('\n'), + icon: MCKINSEY_ICON, spec: { "id": "mckinsey", "label": "McKinsey", diff --git a/packages/flint-js/src/core/theme/presets/nature.ts b/packages/flint-js/src/core/theme/presets/nature.ts index 618ff535..07e3983b 100644 --- a/packages/flint-js/src/core/theme/presets/nature.ts +++ b/packages/flint-js/src/core/theme/presets/nature.ts @@ -2,6 +2,7 @@ // Licensed under the MIT License. import type { ThemePreset } from '../types'; +import { NATURE_ICON } from './icons'; /** * Nature. @@ -16,6 +17,7 @@ export const nature: ThemePreset = { "- Annotate every measure with `unit` in `semantic_types`; `subtitle` names the sample, not the unit.", "- Colour can tell 6 categories apart; past that they share a grey.", ].join('\n'), + icon: NATURE_ICON, spec: { "id": "nature", "label": "Nature", diff --git a/packages/flint-js/src/core/theme/presets/nyt.ts b/packages/flint-js/src/core/theme/presets/nyt.ts index a8bd189a..fca7e743 100644 --- a/packages/flint-js/src/core/theme/presets/nyt.ts +++ b/packages/flint-js/src/core/theme/presets/nyt.ts @@ -2,6 +2,7 @@ // Licensed under the MIT License. import type { ThemePreset } from '../types'; +import { NYT_ICON } from './icons'; /** * New York Times. @@ -16,6 +17,7 @@ export const nyt: ThemePreset = { "- `title` is the finding, in a sentence; `subtitle` names the measure, the population and the unit.", "- Colour can tell 5 categories apart; past that they share a grey.", ].join('\n'), + icon: NYT_ICON, spec: { "id": "nyt", "label": "New York Times", diff --git a/packages/flint-js/src/core/theme/presets/powerbi-light.ts b/packages/flint-js/src/core/theme/presets/powerbi-light.ts index 2f3bd8a1..f7da5e3a 100644 --- a/packages/flint-js/src/core/theme/presets/powerbi-light.ts +++ b/packages/flint-js/src/core/theme/presets/powerbi-light.ts @@ -2,6 +2,7 @@ // Licensed under the MIT License. import type { ThemePreset } from '../types'; +import { POWERBI_LIGHT_ICON } from './icons'; /** * Power BI — light. @@ -20,6 +21,7 @@ export const powerbiLight: ThemePreset = { "- Leave `title` out where the tile sits under its own caption — the axis titles come back to name the measure.", "- Colour can tell 6 categories apart.", ].join('\n'), + icon: POWERBI_LIGHT_ICON, spec: { "id": "powerbi-light", "label": "Power BI (light)", diff --git a/packages/flint-js/src/core/theme/presets/powerbi.ts b/packages/flint-js/src/core/theme/presets/powerbi.ts index 94353b09..d2108b05 100644 --- a/packages/flint-js/src/core/theme/presets/powerbi.ts +++ b/packages/flint-js/src/core/theme/presets/powerbi.ts @@ -2,6 +2,7 @@ // Licensed under the MIT License. import type { ThemePreset } from '../types'; +import { POWERBI_ICON } from './icons'; /** * Power BI. @@ -16,6 +17,7 @@ export const powerbi: ThemePreset = { "- Leave `title` out where the tile sits under its own caption — the axis titles come back to name the measure.", "- Colour can tell 6 categories apart.", ].join('\n'), + icon: POWERBI_ICON, spec: { "id": "powerbi", "label": "Power BI", diff --git a/packages/flint-js/src/core/theme/presets/swiss.ts b/packages/flint-js/src/core/theme/presets/swiss.ts index 1b14fc39..e2abfe52 100644 --- a/packages/flint-js/src/core/theme/presets/swiss.ts +++ b/packages/flint-js/src/core/theme/presets/swiss.ts @@ -2,6 +2,7 @@ // Licensed under the MIT License. import type { ThemePreset } from '../types'; +import { SWISS_ICON } from './icons'; /** * Swiss / International Typographic Style. @@ -29,6 +30,7 @@ export const swiss: ThemePreset = { '- Annotate the measure with `unit` in `semantic_types`.', '- Colour is a single signal red; the categorical key tells 5 series apart.', ].join('\n'), + icon: SWISS_ICON, spec: { id: 'swiss', label: 'Swiss', diff --git a/packages/flint-js/src/core/theme/types.ts b/packages/flint-js/src/core/theme/types.ts index c5064407..f1d0e096 100644 --- a/packages/flint-js/src/core/theme/types.ts +++ b/packages/flint-js/src/core/theme/types.ts @@ -514,6 +514,19 @@ export interface ThemePreset { description: string; /** A few markdown bullets: what this house needs the chart spec to do. */ guidance: string; + /** + * A 16px SVG standing in for the house in a picker, as a complete document + * so a caller can drop it straight into an `` or inline it. + * + * It is drawn from the house's own decisions rather than invented: the tile + * is its canvas, the bars are the first three of its categorical set, and + * the one thing left over says what the house does that the others do not — + * the Economist's red tab, Swiss's structural black rules, McKinsey's + * horizontal bars, Nature's bare axis, cartoon's rounded tops. At this size + * that is all a reader can take in, and it is enough to recognise the house + * once they have seen one chart in it. + */ + icon: string; spec: ThemeSpec; } diff --git a/packages/flint-js/tests/theme-presets.test.ts b/packages/flint-js/tests/theme-presets.test.ts index 403ba270..71e7d018 100644 --- a/packages/flint-js/tests/theme-presets.test.ts +++ b/packages/flint-js/tests/theme-presets.test.ts @@ -4,7 +4,7 @@ import { describe, it, expect } from 'vitest'; import { assembleVegaLite } from '../src'; import { markTypeOf } from '../src/vegalite/theme'; -import { THEME_PRESETS, listThemePresets, resolveThemeSpec } from '../src/core/theme/presets'; +import { THEME_PRESETS, DEFAULT_THEME_ICON, listThemePresets, resolveThemeSpec } from '../src/core/theme/presets'; import type { ThemeSpec } from '../src/core/theme/types'; /** @@ -1327,3 +1327,65 @@ describe('legibility on the house surface', () => { }); } }); + +describe('house icons', () => { + // A picker has to say what a house looks like before the reader has seen a + // chart in it. The icons are how it does that, so they have to be present, + // well-formed, and — the point of the set — distinguishable from each other. + const icons = Object.entries(THEME_PRESETS).map(([id, preset]) => [id, preset.icon] as const); + + for (const [id, icon] of icons) { + it(`${id}: ships a self-contained 16px SVG`, () => { + expect(icon).toMatch(/^$/); + // Self-contained: a bare `` element without the namespace does + // not render from an `` or a data URL. + expect(icon).toContain('xmlns="http://www.w3.org/2000/svg"'); + expect(icon).toContain('viewBox="0 0 16 16"'); + // No external references — an icon that fetches is an icon that + // fails offline, and these ship inside the package. + expect(icon).not.toMatch(/\b(?:href|src|url\()/); + }); + + it(`${id}: is drawn in colours the house actually uses`, () => { + const spec: any = THEME_PRESETS[id].spec; + const declared = new Set( + JSON.stringify(spec) + .match(/#[0-9a-fA-F]{6}/g) + ?.map((hex) => hex.toLowerCase()) ?? [], + ); + const series: string[] = spec.ink?.series?.categorical ?? []; + // The three bars are the first three of the house's own set: an icon + // that invents a palette is a promise the chart will not keep. + for (const hex of series.slice(0, 3)) expect(icon.toLowerCase()).toContain(hex.toLowerCase()); + // The tile is the house's canvas where it states one. + const canvas = spec.ink?.surface?.canvas; + if (canvas) expect(icon.toLowerCase()).toContain(String(canvas).toLowerCase()); + expect(declared.size).toBeGreaterThan(0); + }); + } + + it('gives every house a different picture', () => { + const seen = new Map(); + for (const [id, icon] of icons) { + const clash = seen.get(icon); + expect(clash, `${id} and ${clash} share an icon`).toBeUndefined(); + seen.set(icon, id); + } + expect(seen.size).toBe(icons.length); + }); + + it('offers "no house" its own picture too', () => { + expect(DEFAULT_THEME_ICON).toMatch(/^ { + // `listThemePresets` is what a model reads to choose a house. An SVG it + // cannot see is context spent for nothing. + for (const entry of listThemePresets()) { + expect(entry).not.toHaveProperty('icon'); + expect(JSON.stringify(entry)).not.toContain('`-ready URL. */ +function themeIconUrl(svg: string): string { + return `data:image/svg+xml,${encodeURIComponent(svg)}`; +} + +const THEME_CHOICES: { id: string | undefined; label: string; icon: string; description: string }[] = [ + { + id: undefined, + label: 'Flint default', + icon: DEFAULT_THEME_ICON, + description: "Flint's own defaults \u2014 no house applied.", + }, + ...Object.values(THEME_PRESETS).map((preset) => ({ + id: preset.id, + label: preset.label, + icon: preset.icon, + description: preset.description, + })), +]; + +/** + * The house switch, first in the bar. A theme is the outermost decision on the + * chart — it settles the surface, the ink and the type that everything the + * other controls touch is then drawn in — so it reads left of them. + * + * It borrows the chart-type switch's chrome rather than growing its own: the + * two are the same gesture (a small picture of the result, a caret, a list), + * and giving them different shapes would suggest they are different kinds of + * control. + */ +function ThemeControl(props: { themeId: string | undefined; onTheme: (id: string | undefined) => void }) { + const { themeId, onTheme } = props; + const [open, setOpen] = useState(false); + const rootRef = useRef(null); + const current = THEME_CHOICES.find((choice) => choice.id === themeId) ?? THEME_CHOICES[0]; + + useEffect(() => { + if (!open) return; + const onDoc = (e: MouseEvent) => { + if (rootRef.current && !rootRef.current.contains(e.target as Node)) setOpen(false); + }; + document.addEventListener('mousedown', onDoc); + return () => document.removeEventListener('mousedown', onDoc); + }, [open]); + + return ( +
+ + + {open && ( +
    + {THEME_CHOICES.map((choice) => { + const selected = choice.id === current.id; + return ( +
  • { + onTheme(choice.id); + setOpen(false); + }} + > + + {choice.label} +
  • + ); + })} +
+ )} +
+ ); +} + function ActionButton(props: { label: string; className?: string; @@ -443,6 +534,10 @@ function OptionsBar(props: { return (
+ onInput(withTheme(input, id))} + /> {((model.chartType && model.chartType.length > 1) || (model.arrange && model.arrange.length > 1)) && ( buildPanelModel(current), [current]); const canReset = useMemo( - () => JSON.stringify(current.chart_spec) !== JSON.stringify(input.chart_spec), - [current.chart_spec, input.chart_spec], + () => + JSON.stringify(current.chart_spec) !== JSON.stringify(input.chart_spec) || + JSON.stringify(current.theme_spec ?? null) !== JSON.stringify(input.theme_spec ?? null), + [current.chart_spec, input.chart_spec, current.theme_spec, input.theme_spec], ); const handleCopyPng = useCallback(async () => { diff --git a/packages/flint-mcp/ui/src/options.ts b/packages/flint-mcp/ui/src/options.ts index 0f8b83e9..411907df 100644 --- a/packages/flint-mcp/ui/src/options.ts +++ b/packages/flint-mcp/ui/src/options.ts @@ -238,6 +238,19 @@ export function setChannelField( return next; } +/** + * Name the house the chart is drawn in, or clear it back to flint's own + * defaults. `theme_spec` sits at the top of the input rather than inside + * `chart_spec` because it is not a property of this chart — the same house + * applies whatever the chart turns out to be. + */ +export function withTheme(input: ChartAssemblyInput, themeId: string | undefined): ChartAssemblyInput { + const next = cloneInput(input); + if (themeId === undefined) delete next.theme_spec; + else next.theme_spec = themeId; + return next; +} + /** * Set (or reset, when value is undefined) a chart property or encoding-action * override. Both are stored under `chart_spec.chartProperties[key]`. diff --git a/site/src/components/ChartCodeModal.tsx b/site/src/components/ChartCodeModal.tsx index af1ad599..3129582a 100644 --- a/site/src/components/ChartCodeModal.tsx +++ b/site/src/components/ChartCodeModal.tsx @@ -49,6 +49,10 @@ export function ChartCodeModal({ // Temporary, display-only overrides driven by the options bar. These tweak // the shown Flint spec JSON without mutating any underlying test case. const [tempOptions, setTempOptions] = useState>({}); + // The house the preview is drawn in. Only Vega-Lite reads `theme_spec`, so + // the switch is offered only there. + const [themeId, setThemeId] = useState(undefined); + const canTheme = chart.backend === 'vegalite'; const testCase = tests[index]; @@ -65,12 +69,13 @@ export function ChartCodeModal({ const base = testCaseToAssemblyInput(testCase); return { ...base, + ...(canTheme && themeId ? { theme_spec: themeId } : {}), chart_spec: { ...base.chart_spec, chartProperties: { ...base.chart_spec.chartProperties, ...tempOptions }, }, }; - }, [testCase, tempOptions]); + }, [testCase, tempOptions, themeId, canTheme]); const panelModel = useMemo( () => (displayInput ? buildPanelModel(displayInput, chart.backend) : null), @@ -234,6 +239,7 @@ export function ChartCodeModal({ testCase={testCase} backend={chart.backend} chartPropertyOverrides={tempOptions} + themeId={themeId} /> )} @@ -266,8 +272,13 @@ export function ChartCodeModal({ 0} - onReset={() => setTempOptions({})} + themeId={themeId} + onTheme={canTheme ? setThemeId : undefined} + canReset={Object.keys(tempOptions).length > 0 || themeId !== undefined} + onReset={() => { + setTempOptions({}); + setThemeId(undefined); + }} onChange={(key, value) => setTempOptions((prev) => { const next = { ...prev }; diff --git a/site/src/components/GalleryOptionsBar.tsx b/site/src/components/GalleryOptionsBar.tsx index 612321cf..b1ef35ae 100644 --- a/site/src/components/GalleryOptionsBar.tsx +++ b/site/src/components/GalleryOptionsBar.tsx @@ -13,6 +13,7 @@ import { useEffect, useRef, useState, type CSSProperties } from 'react'; import { useTranslation } from 'react-i18next'; import type { ChartOption } from 'flint-chart'; +import { THEME_PRESETS, DEFAULT_THEME_ICON } from 'flint-chart'; import { siteTheme } from '../shared/theme'; import { chartIconFor } from '../shared/chart-categories'; import type { ControlSpec, PanelModel, ResolvedAction } from '../shared/chart-options'; @@ -102,6 +103,151 @@ function DiscreteControl(props: { ); } +/** A theme preset's icon as an ``-ready URL. */ +function iconUrl(svg: string): string { + return `data:image/svg+xml,${encodeURIComponent(svg)}`; +} + +const THEME_CHOICES: { id: string | undefined; label: string; icon: string; description: string }[] = [ + { + id: undefined, + label: 'Flint default', + icon: DEFAULT_THEME_ICON, + description: "Flint's own defaults — no house applied.", + }, + ...Object.values(THEME_PRESETS).map((preset) => ({ + id: preset.id, + label: preset.label, + icon: preset.icon, + description: preset.description, + })), +]; + +/** + * The house switch. First in the bar, because a theme is the outermost + * decision on the chart: it settles the surface, the ink and the type that + * everything the other controls touch is then drawn in. + * + * Only Vega-Lite reads `theme_spec`, so on the other backends the control is + * absent rather than inert — a switch that does nothing reads as a bug in the + * theme, which is exactly what themes must never look like. + */ +function ThemeControl(props: { themeId: string | undefined; onTheme: (id: string | undefined) => void }) { + const { themeId, onTheme } = props; + const [open, setOpen] = useState(false); + const [hover, setHover] = useState(false); + const rootRef = useRef(null); + const current = THEME_CHOICES.find((choice) => choice.id === themeId) ?? THEME_CHOICES[0]; + + useEffect(() => { + if (!open) return; + const onDoc = (event: MouseEvent) => { + if (rootRef.current && !rootRef.current.contains(event.target as Node)) setOpen(false); + }; + document.addEventListener('mousedown', onDoc); + return () => document.removeEventListener('mousedown', onDoc); + }, [open]); + + return ( +
{ + if (event.key === 'Escape' && open) { + event.preventDefault(); + event.stopPropagation(); + setOpen(false); + } + }} + > + + + {open && ( +
    + {THEME_CHOICES.map((choice) => { + const selected = choice.id === current.id; + return ( +
  • { + onTheme(choice.id); + setOpen(false); + }} + style={{ + display: 'flex', + alignItems: 'center', + gap: 8, + padding: '6px 8px', + borderRadius: 7, + cursor: 'pointer', + fontSize: 12, + color: siteTheme.text, + background: selected ? 'rgba(0,0,0,0.06)' : 'transparent', + }} + onMouseEnter={(e) => { + if (!selected) (e.currentTarget as HTMLLIElement).style.background = 'rgba(0,0,0,0.04)'; + }} + onMouseLeave={(e) => { + if (!selected) (e.currentTarget as HTMLLIElement).style.background = 'transparent'; + }} + > + + {choice.label} +
  • + ); + })} +
+ )} +
+ ); +} + function ControlRow(props: { label: string; spec: ControlSpec; @@ -376,9 +522,12 @@ export function GalleryOptionsBar(props: { onReset: () => void; canReset: boolean; chartType: string; + /** Current house, or undefined for flint's own defaults. Omit to hide the switch. */ + themeId?: string; + onTheme?: (id: string | undefined) => void; style?: CSSProperties; }) { - const { model, onChange, onReset, canReset, chartType, style } = props; + const { model, onChange, onReset, canReset, chartType, themeId, onTheme, style } = props; const controls: { key: string; label: string; spec: ControlSpec; value: unknown }[] = [ ...model.properties.map((option: ChartOption) => ({ @@ -397,6 +546,7 @@ export function GalleryOptionsBar(props: { return (
+ {onTheme && } {((model.chartType && model.chartType.length > 1) || (model.arrange && model.arrange.length > 1)) && ( ; + /** + * House to draw in, named by preset id. Only the Vega-Lite assembler reads + * `theme_spec`, so it is left off elsewhere rather than passed and ignored. + */ + themeId?: string; }) { const input = useMemo(() => { const base = testCaseToAssemblyInput(testCase, canvasSize ?? thumbnailCanvasSize(testCase)); + const themed = themeId && backend === 'vegalite' ? { ...base, theme_spec: themeId } : base; if (!chartPropertyOverrides || Object.keys(chartPropertyOverrides).length === 0) { - return base; + return themed; } return { - ...base, + ...themed, chart_spec: { - ...base.chart_spec, - chartProperties: { ...base.chart_spec.chartProperties, ...chartPropertyOverrides }, + ...themed.chart_spec, + chartProperties: { ...themed.chart_spec.chartProperties, ...chartPropertyOverrides }, }, }; - }, [testCase, canvasSize, chartPropertyOverrides]); + }, [testCase, canvasSize, chartPropertyOverrides, themeId, backend]); const compiled = useMemo(() => { try { diff --git a/site/src/routes/Landing.tsx b/site/src/routes/Landing.tsx index 41b05a67..537ea3f0 100644 --- a/site/src/routes/Landing.tsx +++ b/site/src/routes/Landing.tsx @@ -491,6 +491,9 @@ function HeroShowcase() { const [exampleIdx, setExampleIdx] = useState(0); const [selectedBackend, setSelectedBackend] = useState('vegalite'); const [tempOptions, setTempOptions] = useState>({}); + // The house the showcase chart is drawn in. Only Vega-Lite reads + // `theme_spec`, so the switch is offered only on that backend. + const [themeId, setThemeId] = useState(undefined); const example = SHOWCASE_EXAMPLES[exampleIdx]; const exampleLabel = t(`landing.examples.${example.exampleKey}.label`); @@ -511,17 +514,20 @@ function HeroShowcase() { useEffect(() => setTempOptions({}), [exampleIdx, backend]); + const canTheme = backend === 'vegalite'; + const activeTheme = canTheme ? themeId : undefined; const displayInput = useMemo(() => { if (!testCase) return null; const base = testCaseToAssemblyInput(testCase); return { ...base, + ...(activeTheme ? { theme_spec: activeTheme } : {}), chart_spec: { ...base.chart_spec, chartProperties: { ...base.chart_spec.chartProperties, ...effectiveOptions }, }, }; - }, [testCase, effectiveOptions]); + }, [testCase, effectiveOptions, activeTheme]); const panelModel = useMemo( () => (displayInput ? buildPanelModel(displayInput, backend) : null), @@ -569,6 +575,7 @@ function HeroShowcase() { testCase={testCase} canvasSize={example.canvasSize} chartPropertyOverrides={effectiveOptions} + themeId={activeTheme} />
@@ -604,6 +611,7 @@ function HeroShowcase() { backend={backend} canvasSize={example.canvasSize} chartPropertyOverrides={effectiveOptions} + themeId={activeTheme} />
@@ -612,8 +620,13 @@ function HeroShowcase() { 0} - onReset={() => setTempOptions({})} + themeId={themeId} + onTheme={canTheme ? setThemeId : undefined} + canReset={Object.keys(tempOptions).length > 0 || themeId !== undefined} + onReset={() => { + setTempOptions({}); + setThemeId(undefined); + }} onChange={(key, value) => setTempOptions((current) => { const next = { ...current }; @@ -722,10 +735,12 @@ function FlintSpecCode({ testCase, canvasSize, chartPropertyOverrides, + themeId, }: { testCase: TestCase; canvasSize?: { width: number; height: number }; chartPropertyOverrides?: Record; + themeId?: string; }) { const text = useMemo(() => { const summary = testCaseToFlintSummary(testCase); @@ -741,10 +756,10 @@ function FlintSpecCode({ } : {}), }; - const withCanvas = { ...summary, chart_spec: chartSpec }; + const withCanvas = { ...summary, ...(themeId ? { theme_spec: themeId } : {}), chart_spec: chartSpec }; const body = JSON.stringify(withCanvas, null, 2); return body.replace(/^{\n/, '{\n "data": {...},\n'); - }, [testCase, canvasSize, chartPropertyOverrides]); + }, [testCase, canvasSize, chartPropertyOverrides, themeId]); return
{text}
; } From 63f9011826a7bf92f073703d33221352d902ee2c Mon Sep 17 00:00:00 2001 From: Chenglong Wang Date: Mon, 3 Aug 2026 08:42:50 -0700 Subject: [PATCH 083/164] theme: let the house's own measure and branding reach the widgets MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Switching houses in either widget changed the colours and left everything else where it was. Two things were being dropped between the Theme Lab and the thing people actually look at. The Economist's red masthead tab never appeared. It is anchored to the graphic frame rather than the plot, which Vega-Lite cannot express, so the theme records its coordinates and the renderer paints it on afterwards — and only the lab and the site were doing that last step. The MCP server and the MCP app both stopped at `toSVG()`. Both now paint it, and the app paints it onto its canvas too, so the image you copy carries the tab rather than quietly losing it. This is the kind of step that goes missing without anyone noticing, because the spec still looks right, so it is now asserted on the artifact. The second was type. A house's `baseSize` is not a stray number: it is the measure the rest of the style was set against — the Economist's wide print column, Nature's narrow single-column figure — and the type scale is read off it. The assembler already ranks a stated size above the house's, which is correct when a person states one. But both widgets were stating one on the reader's behalf: the app restated flint's default as a preview size, and the gallery passed the per-chart-type canvas it uses to shape its tiles. Neither is a decision anybody made about this chart, so neither should have outranked the house — and the result was that every house came out at the same size in the same type, which is the one thing a set of houses must not do. Now they stand down to a ceiling when a house is chosen, and headlines range from Nature's 12.5px to the NYT's 17px, matching the lab exactly. The gallery's version lives in one `withHouse` helper shared by the chart and by the spec shown beside it, so the code you copy stays the code that was rendered. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 175e0e03-5dda-4a02-aa14-c181660b9c99 --- packages/flint-mcp/src/render/vegalite.ts | 7 +++- packages/flint-mcp/tests/render.test.ts | 26 +++++++++++++ packages/flint-mcp/ui/src/render.ts | 45 +++++++++++++++++++++-- site/src/components/ChartCodeModal.tsx | 5 +-- site/src/components/WallChart.tsx | 4 +- site/src/routes/Landing.tsx | 7 ++-- site/src/shared/test-case-utils.ts | 30 +++++++++++++++ 7 files changed, 111 insertions(+), 13 deletions(-) diff --git a/packages/flint-mcp/src/render/vegalite.ts b/packages/flint-mcp/src/render/vegalite.ts index 098234fe..3cb6f532 100644 --- a/packages/flint-mcp/src/render/vegalite.ts +++ b/packages/flint-mcp/src/render/vegalite.ts @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +import { injectCanvasFurnitureSVG, readCanvasFurniture } from 'flint-chart'; import { CHART_FONT_FAMILY, installVegaTextMetrics } from './fonts.js'; import { svgToResult } from './svg.js'; import type { RenderResult, RenderFormat } from './types.js'; @@ -52,7 +53,11 @@ export async function renderVegaLite( const view = new vega.View(runtime, { renderer: 'none' }); view.logLevel(vega.Error); await view.runAsync(); - const svg = await view.toSVG(); + // A house may anchor branding to the graphic frame rather than the plot — + // the Economist's red masthead tab. Vega-Lite cannot express that, so the + // theme records the coordinates and the renderer paints it on afterwards. + // Done before rasterising, so the PNG carries it too. + const svg = injectCanvasFurnitureSVG(await view.toSVG(), readCanvasFurniture(spec)); view.finalize(); // Vega's View width/height report the content box, excluding axes/legends. diff --git a/packages/flint-mcp/tests/render.test.ts b/packages/flint-mcp/tests/render.test.ts index 4f1df9d4..ccc89b20 100644 --- a/packages/flint-mcp/tests/render.test.ts +++ b/packages/flint-mcp/tests/render.test.ts @@ -116,3 +116,29 @@ describe('input guards', () => { } }); }); + +describe('house styling survives the render', () => { + // The Economist's red masthead tab is anchored to the graphic frame, not the + // plot, so Vega-Lite cannot draw it — the theme records its coordinates and + // the renderer paints it on afterwards. That last step is easy to lose (the + // spec still looks correct), so it is asserted on the artifact. + it('paints canvas-anchored furniture into the SVG', async () => { + const plain = await renderChart(sales, 'vegalite', { format: 'svg' }); + const themed = await renderChart( + { ...sales, theme_spec: 'economist' }, + 'vegalite', + { format: 'svg' }, + ); + expect(plain.svg).not.toContain('#e3120b'); + expect(themed.svg).toContain('#e3120b'); + }); + + it('carries the tab through to the PNG', async () => { + const themed = await renderChart( + { ...sales, theme_spec: 'economist' }, + 'vegalite', + { format: 'png' }, + ); + expect(themed.buffer!.length).toBeGreaterThan(1000); + }); +}); diff --git a/packages/flint-mcp/ui/src/render.ts b/packages/flint-mcp/ui/src/render.ts index ebce92d8..dee7135b 100644 --- a/packages/flint-mcp/ui/src/render.ts +++ b/packages/flint-mcp/ui/src/render.ts @@ -7,7 +7,7 @@ * the chart re-renders instantly as the user edits options. No server round * trip, no data leaving the host. */ -import { assembleVegaLite } from 'flint-chart'; +import { assembleVegaLite, injectCanvasFurnitureSVG, readCanvasFurniture, resolveThemeSpec } from 'flint-chart'; import type { ChartAssemblyInput } from 'flint-chart'; import { compile } from 'vega-lite'; import { parse, View, Error as VegaError } from 'vega'; @@ -44,13 +44,36 @@ function usesAutoPreviewSize(input: ChartAssemblyInput): boolean { return !input.chart_spec.baseSize && !input.chart_spec.canvasSize; } +/** + * The footprint the chosen house draws at, if it states one. + * + * A house's `baseSize` is not a stray number: it is the measure the rest of the + * style was set against — the Economist's wide print column, Nature's narrow + * single-column figure — and the type scale is read off it. Handing the + * assembler the app's own preview size instead pre-empts it, and every house + * comes out at the same size in the same type, which is the one thing a set of + * houses must not do. + */ +function houseBaseSize(input: ChartAssemblyInput): { width: number; height: number } | undefined { + try { + return resolveThemeSpec(input.theme_spec)?.compileDefaults?.baseSize; + } catch { + // An unknown house is the assembler's error to report, not ours to swallow. + return undefined; + } +} + function withAppPreviewDefaults(input: ChartAssemblyInput): ChartAssemblyInput { if (!usesAutoPreviewSize(input)) return input; + // The ceiling is the widget's business — it is how much room there is. The + // footprint is the house's, and the app's preview size only stands in where + // no house has spoken. + const base = houseBaseSize(input) ?? APP_PREVIEW_BASE_SIZE; return { ...input, chart_spec: { ...input.chart_spec, - baseSize: { ...APP_PREVIEW_BASE_SIZE }, + baseSize: { ...base }, canvasSize: { ...APP_PREVIEW_CANVAS_SIZE }, }, }; @@ -129,7 +152,23 @@ export async function renderFlintSvg( view.logLevel(VegaError); await view.runAsync(); const pngScale = copyPngScale(view.width(), view.height()); - const [svg, canvas] = await Promise.all([view.toSVG(), view.toCanvas(pngScale)]); + const [rawSvg, canvas] = await Promise.all([view.toSVG(), view.toCanvas(pngScale)]); + // Canvas-anchored furniture (the Economist masthead tab) is painted after + // Vega is done, because it belongs to the graphic frame rather than the plot + // and Vega-Lite has no way to say so. Both artifacts get it: the SVG by + // markup, the PNG by drawing straight onto the same view's canvas at the + // scale it was rasterised at. + const furniture = readCanvasFurniture(vlSpec); + const svg = injectCanvasFurnitureSVG(rawSvg, furniture); + if (furniture.length) { + const ctx = canvas.getContext('2d') as CanvasRenderingContext2D | null; + if (ctx) { + for (const item of furniture) { + ctx.fillStyle = item.color; + ctx.fillRect(item.x * pngScale, item.y * pngScale, item.width * pngScale, item.height * pngScale); + } + } + } const png = await new Promise((resolve, reject) => { canvas.toBlob( (blob) => blob ? resolve(blob) : reject(new Error('Could not encode chart PNG')), diff --git a/site/src/components/ChartCodeModal.tsx b/site/src/components/ChartCodeModal.tsx index 3129582a..ddb713a2 100644 --- a/site/src/components/ChartCodeModal.tsx +++ b/site/src/components/ChartCodeModal.tsx @@ -5,7 +5,7 @@ import { JsonCodeMirror } from './JsonCodeMirror'; import { ScaleToFit } from './ScaleToFit'; import { WallChart } from './WallChart'; import { GalleryOptionsBar } from './GalleryOptionsBar'; -import { testCaseToAssemblyInput } from '../shared/test-case-utils'; +import { testCaseToAssemblyInput, withHouse } from '../shared/test-case-utils'; import { buildPanelModel } from '../shared/chart-options'; import { buildGalleryEditorHref } from '../shared/editor-payload'; import { useLocale } from '../i18n/LocaleContext'; @@ -66,10 +66,9 @@ export function ChartCodeModal({ // actions are stored). Display only — never persisted. const displayInput = useMemo(() => { if (!testCase) return null; - const base = testCaseToAssemblyInput(testCase); + const base = withHouse(testCaseToAssemblyInput(testCase), canTheme ? themeId : undefined); return { ...base, - ...(canTheme && themeId ? { theme_spec: themeId } : {}), chart_spec: { ...base.chart_spec, chartProperties: { ...base.chart_spec.chartProperties, ...tempOptions }, diff --git a/site/src/components/WallChart.tsx b/site/src/components/WallChart.tsx index 51483885..aef6b9d6 100644 --- a/site/src/components/WallChart.tsx +++ b/site/src/components/WallChart.tsx @@ -4,7 +4,7 @@ import { VegaLiteView } from './VegaLiteView'; import { EChartsView } from './EChartsView'; import { ChartjsView } from './ChartjsView'; import { PlotlyView } from './PlotlyView'; -import { testCaseToAssemblyInput, thumbnailCanvasSize, type CanvasSize } from '../shared/test-case-utils'; +import { testCaseToAssemblyInput, thumbnailCanvasSize, withHouse, type CanvasSize } from '../shared/test-case-utils'; import { BACKENDS, type PreviewBackend } from '../shared/supported-backends'; import { siteTheme } from '../shared/theme'; @@ -37,7 +37,7 @@ export function WallChart({ }) { const input = useMemo(() => { const base = testCaseToAssemblyInput(testCase, canvasSize ?? thumbnailCanvasSize(testCase)); - const themed = themeId && backend === 'vegalite' ? { ...base, theme_spec: themeId } : base; + const themed: any = withHouse(base, themeId && backend === 'vegalite' ? themeId : undefined); if (!chartPropertyOverrides || Object.keys(chartPropertyOverrides).length === 0) { return themed; } diff --git a/site/src/routes/Landing.tsx b/site/src/routes/Landing.tsx index 537ea3f0..4c446569 100644 --- a/site/src/routes/Landing.tsx +++ b/site/src/routes/Landing.tsx @@ -10,7 +10,7 @@ import { WallChart } from '../components/WallChart'; import { ScaleToFit } from '../components/ScaleToFit'; import { GalleryOptionsBar } from '../components/GalleryOptionsBar'; import { SpecPipelineFigure } from '../components/SpecPipelineFigure'; -import { testCaseToFlintSummary, testCaseToAssemblyInput } from '../shared/test-case-utils'; +import { testCaseToFlintSummary, testCaseToAssemblyInput, withHouse } from '../shared/test-case-utils'; import { buildGalleryEditorHref, openEditorWithPayload } from '../shared/editor-payload'; import { buildPanelModel } from '../shared/chart-options'; import { CHART_CATEGORIES } from '../shared/chart-categories'; @@ -518,10 +518,9 @@ function HeroShowcase() { const activeTheme = canTheme ? themeId : undefined; const displayInput = useMemo(() => { if (!testCase) return null; - const base = testCaseToAssemblyInput(testCase); + const base = withHouse(testCaseToAssemblyInput(testCase), activeTheme); return { ...base, - ...(activeTheme ? { theme_spec: activeTheme } : {}), chart_spec: { ...base.chart_spec, chartProperties: { ...base.chart_spec.chartProperties, ...effectiveOptions }, @@ -756,7 +755,7 @@ function FlintSpecCode({ } : {}), }; - const withCanvas = { ...summary, ...(themeId ? { theme_spec: themeId } : {}), chart_spec: chartSpec }; + const withCanvas = withHouse({ ...summary, chart_spec: chartSpec }, themeId); const body = JSON.stringify(withCanvas, null, 2); return body.replace(/^{\n/, '{\n "data": {...},\n'); }, [testCase, canvasSize, chartPropertyOverrides, themeId]); diff --git a/site/src/shared/test-case-utils.ts b/site/src/shared/test-case-utils.ts index a141241f..4ce2905f 100644 --- a/site/src/shared/test-case-utils.ts +++ b/site/src/shared/test-case-utils.ts @@ -158,3 +158,33 @@ export function thumbnailCanvasSize(t: TestCase): CanvasSize { if (cardinality === 0 || cardinality > LOW_CARDINALITY_MAX) return DEFAULT_CANVAS; return THUMBNAIL_WIDE_CANVAS; } + +/** + * How far a themed chart may stretch when the house's own footprint drives it. + * Square and generous, so a house that wants to run wide (a long category + * ruler) or tall (a stack of small multiples) can, without the ceiling + * dictating the shape it starts from. + */ +const THEMED_CANVAS_CEILING: CanvasSize = { width: 720, height: 720 }; + +/** + * Name a house on an assembly input, and stand the gallery's own sizing down. + * + * A house states the footprint its style was measured against — the + * Economist's wide print column, Nature's narrow single-column figure — and + * its type scale is read off that footprint. The gallery's per-chart-type + * canvas is a tile-shaping convenience, not a decision anyone made about this + * chart, so when a house is chosen it steps aside and becomes a ceiling + * instead. That is exactly what the Theme Lab does, which is why the two agree. + */ +export function withHouse }>( + input: T, + themeId: string | undefined, +): T { + if (!themeId) return input; + return { + ...input, + chart_spec: { ...input.chart_spec, baseSize: undefined, canvasSize: THEMED_CANVAS_CEILING }, + theme_spec: themeId, + } as T; +} From d32d3e0077657704f2cea69304211367d21ef296 Mon Sep 17 00:00:00 2001 From: Chenglong Wang Date: Mon, 3 Aug 2026 10:49:00 -0700 Subject: [PATCH 084/164] theme: let a new house speak over options nobody really chose MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The render fixtures carried no headline. A titled chart is what people actually ask for, and the title is the first thing a house styles — the Economist even hangs its masthead tab off it — so a titleless fixture was testing a chart nobody sends. All three now carry a title and a deck. The larger fix: a house does not only restyle a chart, it can change what the chart *is*. The NYT puts points on a line; Nature puts points in a box plot. Those arrive as defaults, so they yield to anything the spec states — which is right, because a reader's decision should outlive a change of house. But the options bar cannot tell a decision from an echo. A control the reader nudged and a control that happens to sit where it was put are stored the same way, as an explicit value, so the first touch of any control pinned it against every house forever. Toggle points off under flint — where they were already off, so nothing happened — then pick the NYT, and the line stays bare. From the reader's side the house simply did not work, and the harder it was fiddled with beforehand, the less of the house came through. So on a change of house the panel now lets go of the values that only restate what the chart would have done unaided. A value that agrees with the chart without it says nothing and is not carried across; a value that disagrees is a real choice and stays. Verified in the direction that matters: `showPoints: false` set under the NYT survives the move to the Economist, because there it means something. `valueKey` moves into each option model, since deciding whether two option values are the same is now a question the model itself asks. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 175e0e03-5dda-4a02-aa14-c181660b9c99 --- packages/flint-mcp/tests/http.test.ts | 5 ++ packages/flint-mcp/tests/options.test.ts | 64 +++++++++++++++++++++++ packages/flint-mcp/tests/render.test.ts | 5 ++ packages/flint-mcp/tests/server.test.ts | 5 ++ packages/flint-mcp/ui/src/FlintApp.tsx | 6 +-- packages/flint-mcp/ui/src/options.ts | 52 +++++++++++++++++- site/src/components/ChartCodeModal.tsx | 13 ++++- site/src/components/GalleryOptionsBar.tsx | 6 +-- site/src/routes/Landing.tsx | 13 ++++- site/src/shared/chart-options.ts | 50 ++++++++++++++++++ 10 files changed, 204 insertions(+), 15 deletions(-) create mode 100644 packages/flint-mcp/tests/options.test.ts diff --git a/packages/flint-mcp/tests/http.test.ts b/packages/flint-mcp/tests/http.test.ts index 43d740ae..60264563 100644 --- a/packages/flint-mcp/tests/http.test.ts +++ b/packages/flint-mcp/tests/http.test.ts @@ -15,6 +15,11 @@ const barChart = { chart_spec: { chartType: 'Bar Chart', encodings: { x: { field: 'region' }, y: { field: 'revenue' } }, + // Real calls carry a headline: it is the first thing a house styles, and + // some (the Economist's masthead tab) hang furniture off it. A titleless + // fixture exercises a chart nobody actually asks for. + title: 'Revenue by region', + subtitle: 'FY2024, $m', baseSize: { width: 320, height: 220 }, }, }; diff --git a/packages/flint-mcp/tests/options.test.ts b/packages/flint-mcp/tests/options.test.ts new file mode 100644 index 00000000..1063306d --- /dev/null +++ b/packages/flint-mcp/tests/options.test.ts @@ -0,0 +1,64 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { describe, it, expect } from 'vitest'; +import type { ChartAssemblyInput } from 'flint-chart'; +import { buildPanelModel, setProperty, withTheme } from '../ui/src/options.js'; + +/** A two-series line chart — the shape the NYT house puts points on. */ +const lines: ChartAssemblyInput = { + data: { + values: [ + { month: '2024-01-01', sales: 3, region: 'North' }, + { month: '2024-02-01', sales: 5, region: 'North' }, + { month: '2024-03-01', sales: 4, region: 'North' }, + { month: '2024-01-01', sales: 2, region: 'South' }, + { month: '2024-02-01', sales: 6, region: 'South' }, + { month: '2024-03-01', sales: 7, region: 'South' }, + ], + }, + semantic_types: { month: 'Date', sales: 'Quantity', region: 'Category' }, + chart_spec: { + chartType: 'Line Chart', + encodings: { x: { field: 'month' }, y: { field: 'sales' }, color: { field: 'region' } }, + title: 'Sales by region', + }, +}; + +function optionValue(input: ChartAssemblyInput, key: string): unknown { + return buildPanelModel(input).properties.find((option) => option.key === key)?.value; +} + +describe('house defaults reach the options bar', () => { + it('shows the house\'s own value for a property nobody has set', () => { + expect(optionValue(lines, 'showPoints')).toBe(false); + expect(optionValue(withTheme(lines, 'nyt'), 'showPoints')).toBe(true); + }); + + it('keeps a value that disagrees with the house it was set under', () => { + const nyt = withTheme(lines, 'nyt'); + const off = setProperty(nyt, 'showPoints', false); + // Said against a house that wanted points — a decision, and it survives. + expect(optionValue(withTheme(off, 'economist'), 'showPoints')).toBe(false); + }); + + it('lets go of a value that only echoed the house it was set under', () => { + // Toggled on and back off under flint: the result says nothing flint had + // not already said, so it must not outrank the next house. + const fiddled = setProperty(lines, 'showPoints', false); + expect(optionValue(fiddled, 'showPoints')).toBe(false); + expect(optionValue(withTheme(fiddled, 'nyt'), 'showPoints')).toBe(true); + }); + + it('does not disturb properties the reader never touched', () => { + const themed = withTheme(setProperty(lines, 'interpolate', 'monotone'), 'nyt'); + expect(optionValue(themed, 'interpolate')).toBe('monotone'); + expect(optionValue(themed, 'showPoints')).toBe(true); + }); + + it('leaves the input alone', () => { + const before = JSON.stringify(lines); + withTheme(setProperty(lines, 'showPoints', false), 'nyt'); + expect(JSON.stringify(lines)).toBe(before); + }); +}); diff --git a/packages/flint-mcp/tests/render.test.ts b/packages/flint-mcp/tests/render.test.ts index ccc89b20..c30d5384 100644 --- a/packages/flint-mcp/tests/render.test.ts +++ b/packages/flint-mcp/tests/render.test.ts @@ -18,6 +18,11 @@ const sales: ChartAssemblyInput = { chart_spec: { chartType: 'Bar Chart', encodings: { x: { field: 'region' }, y: { field: 'revenue' } }, + // Real calls carry a headline: it is the first thing a house styles, and + // some (the Economist's masthead tab) hang furniture off it. A titleless + // fixture exercises a chart nobody actually asks for. + title: 'Revenue by region', + subtitle: 'FY2024, $m', baseSize: { width: 360, height: 240 }, }, }; diff --git a/packages/flint-mcp/tests/server.test.ts b/packages/flint-mcp/tests/server.test.ts index cd841bcb..ae43c173 100644 --- a/packages/flint-mcp/tests/server.test.ts +++ b/packages/flint-mcp/tests/server.test.ts @@ -19,6 +19,11 @@ const barChart = { chart_spec: { chartType: 'Bar Chart', encodings: { x: { field: 'region' }, y: { field: 'revenue' } }, + // Real calls carry a headline: it is the first thing a house styles, and + // some (the Economist's masthead tab) hang furniture off it. A titleless + // fixture exercises a chart nobody actually asks for. + title: 'Revenue by region', + subtitle: 'FY2024, $m', baseSize: { width: 320, height: 220 }, }, }; diff --git a/packages/flint-mcp/ui/src/FlintApp.tsx b/packages/flint-mcp/ui/src/FlintApp.tsx index dcbc1f82..636415e7 100644 --- a/packages/flint-mcp/ui/src/FlintApp.tsx +++ b/packages/flint-mcp/ui/src/FlintApp.tsx @@ -22,6 +22,7 @@ import { chartIconFor } from './chart-icons'; import { buildPanelModel, setProperty, + valueKey, withTheme, type PanelModel, type ResolvedAction, @@ -35,11 +36,6 @@ type ControlSpec = | { type: 'discrete'; options: { value: unknown; label: string }[] } | { type: 'binary' }; -/** Stable string key for an arbitrary option value (handles undefined/objects). */ -function valueKey(value: unknown): string { - return JSON.stringify(value ?? null); -} - function compactSelectLabel(label: string): string { const withoutHint = label.replace(/\s*\([^)]*\)\s*$/u, '').trim(); if (withoutHint.length <= 16) return withoutHint; diff --git a/packages/flint-mcp/ui/src/options.ts b/packages/flint-mcp/ui/src/options.ts index 411907df..b5466759 100644 --- a/packages/flint-mcp/ui/src/options.ts +++ b/packages/flint-mcp/ui/src/options.ts @@ -30,6 +30,11 @@ import type { RawEncodingValue, } from 'flint-chart'; +/** Stable string key for an arbitrary option value (handles undefined/objects). */ +export function valueKey(value: unknown): string { + return JSON.stringify(value ?? null); +} + /** A resolved encoding action ready for rendering (control + current value). */ export interface ResolvedAction { key: string; @@ -238,14 +243,59 @@ export function setChannelField( return next; } +/** + * Drop the chart properties that only restate what the current configuration + * would have chosen anyway. + * + * A control the reader nudged and a control that happens to sit where it was + * put are stored the same way — as an explicit value — so once written, a + * value outranks every house forever. That is right when it is a decision and + * wrong when it is an echo, and after a theme change the difference is + * visible: the reader picks a house whose line carries points and gets a bare + * line, because a `showPoints: false` identical to the old house's default is + * still sitting there outranking it. + * + * A value that agrees with what the chart would have done unaided says + * nothing, so it is not carried across. Anything that disagrees is a real + * choice and stays. + */ +function withoutEchoedProperties(input: ChartAssemblyInput): ChartAssemblyInput { + const stated = input.chart_spec.chartProperties; + const keys = Object.keys(stated ?? {}); + if (!keys.length) return input; + const kept: Record = {}; + for (const key of keys) { + const rest = { ...stated }; + delete rest[key]; + const unaided = { ...input, chart_spec: { ...input.chart_spec, chartProperties: rest } }; + let fallback: unknown; + try { + fallback = getChartOptions(unaided).find((option) => option.key === key)?.value; + } catch { + // The chart does not compile without it — not ours to judge; keep it. + fallback = undefined; + } + if (fallback === undefined || valueKey(fallback) !== valueKey(stated![key])) { + kept[key] = stated![key]; + } + } + return { ...input, chart_spec: { ...input.chart_spec, chartProperties: kept } }; +} + /** * Name the house the chart is drawn in, or clear it back to flint's own * defaults. `theme_spec` sits at the top of the input rather than inside * `chart_spec` because it is not a property of this chart — the same house * applies whatever the chart turns out to be. + * + * A house does not only restyle a chart; it can change what the chart *is* — + * the NYT puts points on a line, Nature puts points in a box plot. Those are + * defaults, so they yield to anything stated, which means the panel has to let + * go of the values it is only holding by inheritance before the new house can + * speak. See {@link withoutEchoedProperties}. */ export function withTheme(input: ChartAssemblyInput, themeId: string | undefined): ChartAssemblyInput { - const next = cloneInput(input); + const next = cloneInput(withoutEchoedProperties(input)); if (themeId === undefined) delete next.theme_spec; else next.theme_spec = themeId; return next; diff --git a/site/src/components/ChartCodeModal.tsx b/site/src/components/ChartCodeModal.tsx index ddb713a2..36c5c92e 100644 --- a/site/src/components/ChartCodeModal.tsx +++ b/site/src/components/ChartCodeModal.tsx @@ -6,7 +6,7 @@ import { ScaleToFit } from './ScaleToFit'; import { WallChart } from './WallChart'; import { GalleryOptionsBar } from './GalleryOptionsBar'; import { testCaseToAssemblyInput, withHouse } from '../shared/test-case-utils'; -import { buildPanelModel } from '../shared/chart-options'; +import { buildPanelModel, withoutEchoedOverrides } from '../shared/chart-options'; import { buildGalleryEditorHref } from '../shared/editor-payload'; import { useLocale } from '../i18n/LocaleContext'; import { humanizeVariants } from '../shared/wall-title'; @@ -76,6 +76,15 @@ export function ChartCodeModal({ }; }, [testCase, tempOptions, themeId, canTheme]); + // A house can change what a chart *is*, not only how it looks — the NYT puts + // points on a line. Those are defaults, so they yield to anything the bar has + // stated, and a value the reader never really chose would silently outrank + // the new house. Let go of the ones that were only echoing the old one. + const chooseTheme = (next: string | undefined) => { + if (displayInput) setTempOptions((options) => withoutEchoedOverrides(displayInput, options)); + setThemeId(next); + }; + const panelModel = useMemo( () => (displayInput ? buildPanelModel(displayInput, chart.backend) : null), [displayInput, chart.backend], @@ -272,7 +281,7 @@ export function ChartCodeModal({ model={panelModel} chartType={displayInput.chart_spec.chartType} themeId={themeId} - onTheme={canTheme ? setThemeId : undefined} + onTheme={canTheme ? chooseTheme : undefined} canReset={Object.keys(tempOptions).length > 0 || themeId !== undefined} onReset={() => { setTempOptions({}); diff --git a/site/src/components/GalleryOptionsBar.tsx b/site/src/components/GalleryOptionsBar.tsx index b1ef35ae..08bc6505 100644 --- a/site/src/components/GalleryOptionsBar.tsx +++ b/site/src/components/GalleryOptionsBar.tsx @@ -16,14 +16,10 @@ import type { ChartOption } from 'flint-chart'; import { THEME_PRESETS, DEFAULT_THEME_ICON } from 'flint-chart'; import { siteTheme } from '../shared/theme'; import { chartIconFor } from '../shared/chart-categories'; +import { valueKey } from '../shared/chart-options'; import type { ControlSpec, PanelModel, ResolvedAction } from '../shared/chart-options'; import './gallery-options-bar.css'; -/** Stable string key for an arbitrary option value (handles undefined/objects). */ -function valueKey(value: unknown): string { - return JSON.stringify(value ?? null); -} - /** Trim a trailing "(hint)" and clip long labels for the compact select. */ function compactSelectLabel(label: string): string { const withoutHint = label.replace(/\s*\([^)]*\)\s*$/u, '').trim(); diff --git a/site/src/routes/Landing.tsx b/site/src/routes/Landing.tsx index 4c446569..db3942b9 100644 --- a/site/src/routes/Landing.tsx +++ b/site/src/routes/Landing.tsx @@ -12,7 +12,7 @@ import { GalleryOptionsBar } from '../components/GalleryOptionsBar'; import { SpecPipelineFigure } from '../components/SpecPipelineFigure'; import { testCaseToFlintSummary, testCaseToAssemblyInput, withHouse } from '../shared/test-case-utils'; import { buildGalleryEditorHref, openEditorWithPayload } from '../shared/editor-payload'; -import { buildPanelModel } from '../shared/chart-options'; +import { buildPanelModel, withoutEchoedOverrides } from '../shared/chart-options'; import { CHART_CATEGORIES } from '../shared/chart-categories'; import { MOVIE_RATINGS } from './movie-ratings-data'; import { @@ -528,6 +528,15 @@ function HeroShowcase() { }; }, [testCase, effectiveOptions, activeTheme]); + // A house can change what a chart *is*, not only how it looks — the NYT puts + // points on a line. Those are defaults, so they yield to anything the bar has + // stated, and a value the reader never really chose would silently outrank + // the new house. Let go of the ones that were only echoing the old one. + const chooseTheme = (next: string | undefined) => { + if (displayInput) setTempOptions((options) => withoutEchoedOverrides(displayInput, options)); + setThemeId(next); + }; + const panelModel = useMemo( () => (displayInput ? buildPanelModel(displayInput, backend) : null), [displayInput, backend], @@ -620,7 +629,7 @@ function HeroShowcase() { model={panelModel} chartType={displayInput.chart_spec.chartType} themeId={themeId} - onTheme={canTheme ? setThemeId : undefined} + onTheme={canTheme ? chooseTheme : undefined} canReset={Object.keys(tempOptions).length > 0 || themeId !== undefined} onReset={() => { setTempOptions({}); diff --git a/site/src/shared/chart-options.ts b/site/src/shared/chart-options.ts index 59994014..4b1b43fd 100644 --- a/site/src/shared/chart-options.ts +++ b/site/src/shared/chart-options.ts @@ -37,6 +37,11 @@ export type ControlSpec = | { type: 'discrete'; options: { value: unknown; label: string }[] } | { type: 'binary' }; +/** Stable string key for an arbitrary option value (handles undefined/objects). */ +export function valueKey(value: unknown): string { + return JSON.stringify(value ?? null); +} + /** A resolved encoding action ready for rendering (control + current value). */ export interface ResolvedAction { key: string; @@ -251,3 +256,48 @@ export function buildPanelModel( return { properties, actions, pivot, chartType: chartTypeSurface, arrange }; } + +/** + * Drop the option overrides that only restate what the chart would have chosen + * anyway. + * + * A control the reader nudged and a control that happens to sit where it was + * put are stored the same way — as an explicit value — so once written, a value + * outranks every house forever. That is right when it is a decision and wrong + * when it is an echo, and after a theme change the difference is visible: the + * reader picks a house whose line carries points and gets a bare line, because + * a `showPoints: false` identical to the old house's default is still sitting + * there outranking it. + * + * A value that agrees with what the chart would have done unaided says nothing, + * so it is not carried across a house change. Anything that disagrees is a real + * choice and stays. + */ +export function withoutEchoedOverrides( + input: ChartAssemblyInput, + overrides: Record, +): Record { + const keys = Object.keys(overrides ?? {}); + if (!keys.length) return overrides; + const stated = { ...(input.chart_spec.chartProperties ?? {}), ...overrides }; + const kept: Record = {}; + for (const key of keys) { + const rest = { ...stated }; + delete rest[key]; + const unaided = { + ...input, + chart_spec: { ...input.chart_spec, chartProperties: rest }, + } as ChartAssemblyInput; + let fallback: unknown; + try { + fallback = getChartOptions(unaided).find((option) => option.key === key)?.value; + } catch { + // The chart does not compile without it — not ours to judge; keep it. + fallback = undefined; + } + if (fallback === undefined || valueKey(fallback) !== valueKey(overrides[key])) { + kept[key] = overrides[key]; + } + } + return kept; +} From 38bb58d32fc519d68390870c2e38fc4c34c0ff7f Mon Sep 17 00:00:00 2001 From: Chenglong Wang Date: Mon, 3 Aug 2026 10:59:18 -0700 Subject: [PATCH 085/164] site: give the MCP app mockup charts worth theming MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit You were right that nothing on the page carried a headline — I patched the vitest fixtures, not the page you were actually looking at. All five mockup charts now carry a title and a deck, because every real call does, and because the headline block is the part of a chart a house has most to say about: the type scale is set on it, and the Economist hangs its masthead tab off it. Without one the theme switch looked like it only recoloured the bars. Four of the five also stop stating a size. A stated size outranks the house's own footprint by design — that is the right order when a person asks for a size — but here nobody had asked; the number was just sitting in the fixture, so every house came out at the same 360x360 in nearly the same type. Unstated, they now land on their own measure: Nature at its narrow single-column figure, the Economist at its wide print column. The sparkline keeps its stated size on purpose, both because packed traces really do want a wide short canvas and so the app's other sizing branch stays covered. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 175e0e03-5dda-4a02-aa14-c181660b9c99 --- site/src/components/McpAppMockup.tsx | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/site/src/components/McpAppMockup.tsx b/site/src/components/McpAppMockup.tsx index f6aa3c85..207fe198 100644 --- a/site/src/components/McpAppMockup.tsx +++ b/site/src/components/McpAppMockup.tsx @@ -10,6 +10,13 @@ type Example = { input: ChartAssemblyInput; }; +// Every fixture carries a headline and a deck, because every real call does. +// They are also the part of a chart a house has most to say about — the type +// scale is set on them and the Economist hangs its masthead tab off them — so +// a titleless fixture makes the theme switch look like it does almost nothing. +// Sizes are left unstated for the same reason: a stated size outranks the +// house's own footprint, and the widget's sizing is one of the things this +// page exists to exercise. const lineInput: ChartAssemblyInput = { data: { values: [ @@ -37,12 +44,12 @@ const lineInput: ChartAssemblyInput = { y: { field: 'commits' }, color: { field: 'area' }, }, + title: 'Where the commits went', + subtitle: 'Commits per month, 2026', chartProperties: { interpolate: 'monotone', showPoints: true, }, - baseSize: { width: 360, height: 360 }, - canvasSize: { width: 720, height: 720 }, }, options: { addTooltips: true }, field_display_names: { @@ -74,9 +81,9 @@ const scatterInput: ChartAssemblyInput = { y: { field: 'mpg' }, color: { field: 'origin' }, }, + title: 'Heavier cars, thirstier engines', + subtitle: 'Miles per gallon against kerb weight, tonnes', chartProperties: { opacity: 0.8 }, - baseSize: { width: 360, height: 360 }, - canvasSize: { width: 720, height: 720 }, }, options: { addTooltips: true }, field_display_names: { weight: 'Weight', mpg: 'MPG', origin: 'Origin' }, @@ -103,9 +110,9 @@ const areaInput: ChartAssemblyInput = { y: { field: 'hours' }, color: { field: 'stage' }, }, + title: 'Build overtakes design', + subtitle: 'Hours logged by stage, 2026', chartProperties: { interpolate: 'monotone' }, - baseSize: { width: 360, height: 360 }, - canvasSize: { width: 720, height: 720 }, }, options: { addTooltips: true }, field_display_names: { month: 'Month', stage: 'Stage', hours: 'Hours' }, @@ -128,9 +135,9 @@ const barInput: ChartAssemblyInput = { x: { field: 'team' }, y: { field: 'wins' }, }, + title: 'The Bears run away with it', + subtitle: 'Wins, regular season', chartProperties: { cornerRadius: 3 }, - baseSize: { width: 360, height: 360 }, - canvasSize: { width: 720, height: 720 }, }, options: { addTooltips: true }, field_display_names: { team: 'Team', wins: 'Wins' }, @@ -162,8 +169,12 @@ const sparklineInput: ChartAssemblyInput = { y: { field: 'latency' }, color: { field: 'service' }, }, + title: 'Latency, service by service', + subtitle: 'Median response in ms, last 14 days', chartProperties: { interpolate: 'monotone' }, // Sparklines want a wide, short canvas: long traces packed into short rows. + // The only fixture that states a size, so the app's other sizing branch — + // caller said, so nobody else gets a say — stays covered here too. baseSize: { width: 720, height: 360 }, canvasSize: { width: 720, height: 360 }, }, From 99e60a93854b5362586095e306ed233620a4d8a7 Mon Sep 17 00:00:00 2001 From: Chenglong Wang Date: Mon, 3 Aug 2026 12:26:15 -0700 Subject: [PATCH 086/164] theme: draw the preview at the size it will be seen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The app rendered into a 720x540 ceiling and displayed the result in a 360px-tall frame, where `.chart`'s `max-height` scaled the finished graphic down to fit. The type went with it: an 11px axis label landed at 7.9px, smaller than the app's own 11px chrome. That, not the theme, is why the charts read as large drawings carrying small type. The frame now tells the assembler how much room it has. Width is the host's to dictate, so it is measured with a ResizeObserver (quantised, so a scrollbar appearing cannot start an oscillation) and passed in. Height is not: a title, deck, legend and axis can take 145px before the plot gets any, and squeezing the plot to fit a short frame shrinks the type faster than the frame saves it — so the frame grows to the chart instead, up to a cap. Step-sized plots spend the width they now know about. Measured on the mcp-ui fixtures: displayed scale 0.70-0.83x -> 0.86-1.00x, swiss line ticks 8.0px -> 11.0px and its headline 12.7px -> 17.5px, and a banded bar fills 90% of the frame rather than 47%. The height cap is a length in both places rather than a percentage. A percentage `max-height` resolves against the parent's height, and a frame that grows to its content has none to give, so the SVG would have drawn full height and the difference would have come back as a scrollbar. A test pins the stylesheet's value to the renderer's. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 175e0e03-5dda-4a02-aa14-c181660b9c99 --- packages/flint-mcp/tests/preview-size.test.ts | 120 +++++++++++++++++ packages/flint-mcp/ui/src/FlintApp.tsx | 43 +++++- packages/flint-mcp/ui/src/render.ts | 124 ++++++++++++++++-- packages/flint-mcp/ui/src/styles.css | 38 ++++-- 4 files changed, 297 insertions(+), 28 deletions(-) create mode 100644 packages/flint-mcp/tests/preview-size.test.ts diff --git a/packages/flint-mcp/tests/preview-size.test.ts b/packages/flint-mcp/tests/preview-size.test.ts new file mode 100644 index 00000000..06303b07 --- /dev/null +++ b/packages/flint-mcp/tests/preview-size.test.ts @@ -0,0 +1,120 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** + * The preview must render into the room it will actually be shown in. + * + * When it renders larger, the `.chart` rule scales the finished SVG down to + * fit and takes the type with it — an 11px axis label displayed at 0.72x lands + * at 7.9px, smaller than the app's own 11px chrome. These tests pin the sizing + * contract that keeps the graphic at 1:1. + */ +import { readFileSync } from 'node:fs'; +import { describe, it, expect } from 'vitest'; +import { compile } from 'vega-lite'; +import { parse, View } from 'vega'; +import type { ChartAssemblyInput } from 'flint-chart'; +import { THEME_PRESETS } from 'flint-chart'; +import { assemblePreviewSpec, APP_PREVIEW_CANVAS_SIZE } from '../ui/src/render.js'; + +/** A titled, decked, banded bar chart — the shape a real tool call produces. */ +function bars(themeId?: string): ChartAssemblyInput { + const input: ChartAssemblyInput = { + data: { + values: [ + { region: 'North', revenue: 128 }, + { region: 'South', revenue: 94 }, + { region: 'East', revenue: 156 }, + { region: 'West', revenue: 112 }, + { region: 'Central', revenue: 88 }, + ], + }, + semantic_types: { region: 'Category', revenue: 'Money' }, + chart_spec: { + chartType: 'Bar Chart', + encodings: { x: { field: 'region' }, y: { field: 'revenue' } }, + title: 'Revenue by region', + subtitle: 'FY2024, $m', + }, + }; + if (themeId) input.theme_spec = THEME_PRESETS[themeId].spec; + return input; +} + +/** The rendered size of a spec, as the browser would lay it out. */ +async function renderedSize(spec: Record): Promise<{ width: number; height: number }> { + const view = new View(parse(compile(spec as never).spec as never, { background: '#fff' } as never), { + renderer: 'none', + }); + await view.runAsync(); + const svg = await view.toSVG(); + view.finalize(); + return { + width: Number(svg.match(/]*\swidth="([\d.]+)"/)?.[1] ?? 0), + height: Number(svg.match(/]*\sheight="([\d.]+)"/)?.[1] ?? 0), + }; +} + +describe('the preview renders into the room it is given', () => { + it('keeps the graphic within the frame it was measured at', async () => { + // 468 is --chart-max-height in styles.css; the frame grows to the chart + // up to that, so only the width has to be respected exactly. + for (const width of [320, 440, 584]) { + for (const house of ['swiss', 'economist', 'nature', 'mckinsey']) { + const size = await renderedSize(assemblePreviewSpec(bars(house), { width })); + // A little overshoot is absorbed by the axis chrome that sits outside + // the plot ceiling; a large one means the type is being scaled down. + expect(size.width, `${house} at ${width}px`).toBeLessThanOrEqual(width * 1.08); + expect(size.height, `${house} at ${width}px`).toBeLessThanOrEqual(468 * 1.05); + } + } + }); + + it('gives a wider frame a wider chart', async () => { + const narrow = await renderedSize(assemblePreviewSpec(bars('swiss'), { width: 320 })); + const wide = await renderedSize(assemblePreviewSpec(bars('swiss'), { width: 584 })); + expect(wide.width).toBeGreaterThan(narrow.width); + }); + + it('ignores a viewport too small to be a real measurement', async () => { + // A collapsed or unmounted box must not shrink the chart to nothing. + const collapsed = await renderedSize(assemblePreviewSpec(bars('swiss'), { width: 0 })); + const absent = await renderedSize(assemblePreviewSpec(bars('swiss'))); + expect(collapsed).toEqual(absent); + }); + + it('still lets a caller state its own size', async () => { + // A stated size opts out of the preview's sizing entirely, so the measured + // frame must make no difference at all. + const stated = () => { + const input = bars('swiss'); + input.chart_spec.baseSize = { width: 700, height: 300 }; + input.chart_spec.canvasSize = { width: 700, height: 300 }; + return input; + }; + const narrowFrame = await renderedSize(assemblePreviewSpec(stated(), { width: 320 })); + const wideFrame = await renderedSize(assemblePreviewSpec(stated(), { width: 584 })); + const noFrame = await renderedSize(assemblePreviewSpec(stated())); + expect(narrowFrame).toEqual(wideFrame); + expect(narrowFrame).toEqual(noFrame); + }); + + it('leaves the house in charge of the type', async () => { + // The frame decides how much room there is, not how big the type is: the + // same house must read the same at any frame width it fits in. + const at = (width: number) => + (assemblePreviewSpec(bars('swiss'), { width }) as any).config?.title?.fontSize; + expect(at(584)).toBe(at(520)); + expect(at(584)).toBeGreaterThan(14); + }); + + it('draws to the same height the stylesheet will allow', () => { + // These two are the same measurement written in two languages. If they + // drift, the chart is drawn to one size and displayed at another, and the + // difference comes back as a scrollbar or as shrunken type. + const css = readFileSync(new URL('../ui/src/styles.css', import.meta.url), 'utf8'); + const declared = css.match(/--chart-max-height:\s*(\d+)px/)?.[1]; + expect(declared, '--chart-max-height missing from styles.css').toBeDefined(); + expect(Number(declared)).toBe(APP_PREVIEW_CANVAS_SIZE.height); + }); +}); diff --git a/packages/flint-mcp/ui/src/FlintApp.tsx b/packages/flint-mcp/ui/src/FlintApp.tsx index 636415e7..22328805 100644 --- a/packages/flint-mcp/ui/src/FlintApp.tsx +++ b/packages/flint-mcp/ui/src/FlintApp.tsx @@ -608,6 +608,35 @@ export function FlintAppInner(props: { const [copyStatus, setCopyStatus] = useState<'idle' | 'copying' | 'copied' | 'downloaded' | 'error'>('idle'); const [copyError, setCopyError] = useState(null); const renderSeq = useRef(0); + // The width the chart actually has. Rendering into the real width means the + // finished SVG is shown at 1:1 rather than being scaled down to fit, which + // is what otherwise shrinks every label below the app's own chrome. Height + // is deliberately not measured: the frame grows to the chart. + const [chartWidth, setChartWidth] = useState(null); + const chartBoxObserver = useRef(null); + + // Measure the chart frame, quantised so a scrollbar appearing and vanishing + // cannot start an oscillation between two neighbouring sizes. + const measureChartBox = useCallback((node: HTMLDivElement | null) => { + chartBoxObserver.current?.disconnect(); + chartBoxObserver.current = null; + if (!node || typeof ResizeObserver === 'undefined') return; + const read = () => { + const style = window.getComputedStyle(node); + // `clientWidth` already excludes any scrollbar, so the reading does not + // shrink in response to the chart it is measuring. + const width = node.clientWidth + - parseFloat(style.paddingLeft || '0') - parseFloat(style.paddingRight || '0'); + if (!(width > 0)) return; + const next = Math.max(0, Math.floor(width / 8) * 8); + setChartWidth((prev) => (prev === next ? prev : next)); + }; + read(); + const observer = new ResizeObserver(read); + observer.observe(node); + chartBoxObserver.current = observer; + }, []); + useEffect(() => () => chartBoxObserver.current?.disconnect(), []); // Re-seed when a new tool input arrives from the host. useEffect(() => setCurrent(input), [input]); @@ -624,7 +653,7 @@ export function FlintAppInner(props: { const seq = ++renderSeq.current; setCopyStatus('idle'); const handle = setTimeout(() => { - renderFlintSvg(current) + renderFlintSvg(current, undefined, chartWidth ? { width: chartWidth } : undefined) .then((result) => { if (seq === renderSeq.current) { setRender(result); @@ -638,7 +667,7 @@ export function FlintAppInner(props: { }); }, 100); return () => clearTimeout(handle); - }, [current]); + }, [current, chartWidth]); const model = useMemo(() => buildPanelModel(current), [current]); const canReset = useMemo( @@ -716,10 +745,14 @@ export function FlintAppInner(props: { Could not render chart
{error}
- ) : render ? ( -
) : ( -
Rendering…
+ // The frame is always mounted, so its size is known before the first + // render and the chart can be assembled to fit it straight away. +
+ {render + ?
+ : Rendering…} +
)} {warnings.length > 0 && ( diff --git a/packages/flint-mcp/ui/src/render.ts b/packages/flint-mcp/ui/src/render.ts index dee7135b..a27387e2 100644 --- a/packages/flint-mcp/ui/src/render.ts +++ b/packages/flint-mcp/ui/src/render.ts @@ -28,7 +28,26 @@ export interface FlintRenderResult { const DEFAULT_BACKGROUND = '#ffffff'; const APP_PREVIEW_BASE_SIZE = { width: 360, height: 270 } as const; -const APP_PREVIEW_CANVAS_SIZE = { width: 720, height: 540 } as const; +/** + * The room the preview has when nobody has measured it. + * + * This is a *ceiling*, not a footprint: it is how far the layout may stretch, + * and it must match the box the SVG is finally shown in. When it does not, the + * `.chart` rule (`max-width/height: 100%`) quietly scales the finished graphic + * down to fit, and the type goes with it — an 11px axis label rendered into a + * 540px-tall frame and displayed in a 348px one lands at 7.9px, smaller than + * the app's own 11px chrome. + * + * Width is the host's to dictate, so the app measures it and passes it in. + * Height is not: the frame grows to whatever the chart needs, because a title, + * a deck, a legend and an axis can easily take 145px before the plot gets any, + * and squeezing the plot to fit a short frame shrinks the type faster than the + * frame saves it. This height is a backstop, and matches `--chart-max-height` + * in `styles.css`. + */ +export const APP_PREVIEW_CANVAS_SIZE = { width: 588, height: 468 } as const; +/** Below this a viewport reading is noise (a collapsed or unmounted box). */ +const MIN_VIEWPORT_WIDTH = 240; const APP_PREVIEW_MIN_STEP_PLOT_SIZE = { width: 220, height: 160 } as const; const APP_PREVIEW_MAX_AUTO_STEP = 96; const COPY_PNG_TARGET_LONG_EDGE = 1920; @@ -63,18 +82,44 @@ function houseBaseSize(input: ChartAssemblyInput): { width: number; height: numb } } -function withAppPreviewDefaults(input: ChartAssemblyInput): ChartAssemblyInput { +/** + * The ceiling to render into: the measured frame width when the app has one, + * the default otherwise. Readings are floored to whole pixels and rejected + * when implausibly small, so a transient zero-size layout pass cannot collapse + * the chart. + */ +function previewCanvasSize(viewport?: { width: number; height?: number }): { width: number; height: number } { + const height = Number.isFinite(viewport?.height) && (viewport!.height as number) > 0 + ? Math.floor(viewport!.height as number) + : APP_PREVIEW_CANVAS_SIZE.height; + const width = Math.floor(viewport?.width ?? NaN); + if (!Number.isFinite(width) || width < MIN_VIEWPORT_WIDTH) { + return { width: APP_PREVIEW_CANVAS_SIZE.width, height }; + } + return { width, height }; +} + +function withAppPreviewDefaults( + input: ChartAssemblyInput, + viewport?: { width: number; height?: number }, +): ChartAssemblyInput { if (!usesAutoPreviewSize(input)) return input; // The ceiling is the widget's business — it is how much room there is. The // footprint is the house's, and the app's preview size only stands in where // no house has spoken. const base = houseBaseSize(input) ?? APP_PREVIEW_BASE_SIZE; + const ceiling = previewCanvasSize(viewport); return { ...input, chart_spec: { ...input.chart_spec, - baseSize: { ...base }, - canvasSize: { ...APP_PREVIEW_CANVAS_SIZE }, + // A footprint larger than the room available is not a footprint; clamp + // it so the house still leads but cannot overflow the frame. + baseSize: { + width: Math.min(base.width, ceiling.width), + height: Math.min(base.height, ceiling.height), + }, + canvasSize: ceiling, }, }; } @@ -95,43 +140,94 @@ function uniqueValueCount(input: ChartAssemblyInput, field: string | undefined): return new Set(rows.map((row) => row?.[field]).filter((value) => value != null)).size; } -function applyStepMinimum(node: unknown, dimension: 'width' | 'height', itemCount: number): void { +/** + * A step-sized plot (one band per category) asks for only as much room as its + * bands need, so five categories can leave half a wide frame empty. When the + * preview knows how much room it has, it spends it: the bands grow until the + * plot fills the frame, less an allowance for the axis, legend and title block + * around it. `APP_PREVIEW_MAX_AUTO_STEP` still caps how fat a single band may + * get, so a two-category chart does not become two enormous slabs. + * + * This runs after assembly, so it only moves the step — the labels keep the + * size and angle chosen for the original one. That is why the cap matters: + * widen far enough and the axis would keep labels rotated as though it were + * still cramped. Letting the assembler size bands to the frame up front is the + * real fix, and belongs in the layout rather than here. + */ +const APP_PREVIEW_CHROME_ALLOWANCE = { width: 110, height: 170 } as const; + +function stepTarget(dimension: 'width' | 'height', ceiling: { width: number; height: number }): number { + return Math.max( + APP_PREVIEW_MIN_STEP_PLOT_SIZE[dimension], + ceiling[dimension] - APP_PREVIEW_CHROME_ALLOWANCE[dimension], + ); +} + +function applyStepMinimum( + node: unknown, + dimension: 'width' | 'height', + itemCount: number, + minPlotSize: number, +): void { if (!node || typeof node !== 'object' || itemCount <= 0) return; const record = node as Record; const size = record[dimension]; if (size && typeof size === 'object') { const stepSize = (size as { step?: unknown }).step; if (typeof stepSize === 'number' && Number.isFinite(stepSize)) { - const minPlotSize = APP_PREVIEW_MIN_STEP_PLOT_SIZE[dimension]; const desiredStep = Math.min(APP_PREVIEW_MAX_AUTO_STEP, Math.ceil(minPlotSize / itemCount)); if (stepSize < desiredStep) { record[dimension] = { ...(size as Record), step: desiredStep }; } } } - applyStepMinimum(record.spec, dimension, itemCount); + applyStepMinimum(record.spec, dimension, itemCount, minPlotSize); } -function widenSmallStepPlotsForPreview(vlSpec: Record, input: ChartAssemblyInput): void { +function widenSmallStepPlotsForPreview( + vlSpec: Record, + input: ChartAssemblyInput, + ceiling: { width: number; height: number }, +): void { const xCount = uniqueValueCount(input, encodingField(input, 'x')); const yCount = uniqueValueCount(input, encodingField(input, 'y')); - applyStepMinimum(vlSpec, 'width', xCount); - applyStepMinimum(vlSpec, 'height', yCount); + applyStepMinimum(vlSpec, 'width', xCount, stepTarget('width', ceiling)); + applyStepMinimum(vlSpec, 'height', yCount, stepTarget('height', ceiling)); +} + +/** + * Assemble the preview's Vega-Lite spec: give the chart the room the frame + * actually has, then let step-sized plots spend it. + * + * Split out from {@link renderFlintSvg} so the sizing can be exercised without + * a browser canvas, which the PNG half of the render needs and Node has not. + */ +export function assemblePreviewSpec( + input: ChartAssemblyInput, + viewport?: { width: number; height?: number }, +): Record { + const usePreviewDefaults = usesAutoPreviewSize(input); + const previewInput = withAppPreviewDefaults(input, viewport); + const spec = assembleVegaLite(previewInput) as Record; + if (usePreviewDefaults) widenSmallStepPlotsForPreview(spec, previewInput, previewCanvasSize(viewport)); + return spec; } /** * Assemble a Flint {@link ChartAssemblyInput} to a Vega-Lite spec and render it * to an SVG string. Throws on assembly or compile failure so the caller can * surface the message. + * + * `viewport` is the width the SVG will be displayed in (and, optionally, a + * height ceiling). Passing it lets the layout use exactly the room it has, so + * the result is shown at 1:1 and the type keeps the size its house chose. */ export async function renderFlintSvg( input: ChartAssemblyInput, background: string = DEFAULT_BACKGROUND, + viewport?: { width: number; height?: number }, ): Promise { - const usePreviewDefaults = usesAutoPreviewSize(input); - const previewInput = withAppPreviewDefaults(input); - const raw = assembleVegaLite(previewInput) as Record; - if (usePreviewDefaults) widenSmallStepPlotsForPreview(raw, previewInput); + const raw = assemblePreviewSpec(input, viewport); const warnings = (raw._warnings as FlintRenderResult['warnings']) ?? []; // Pass the assembled spec straight through: Vega-Lite ignores unknown // top-level keys, so Flint's private annotations (`_warnings`, `_width`, diff --git a/packages/flint-mcp/ui/src/styles.css b/packages/flint-mcp/ui/src/styles.css index 8b0a4ea7..9981a313 100644 --- a/packages/flint-mcp/ui/src/styles.css +++ b/packages/flint-mcp/ui/src/styles.css @@ -15,6 +15,9 @@ --font: Arial, Roboto, "Helvetica Neue", sans-serif; --option-cell-width: 176px; --option-readout-width: 44px; + /* The tallest a chart may be drawn. Keep in step with the height in + APP_PREVIEW_CANVAS_SIZE (packages/flint-mcp/ui/src/render.ts). */ + --chart-max-height: 468px; } * { @@ -53,35 +56,52 @@ body { min-width: 0; } +/* The chart frame. Its width is measured and handed to the assembler, so the + SVG should already fit across; the `max-*` rules are a backstop for specs + that state their own size and so opt out of the preview's sizing. + + Height is deliberately not fixed. A title, deck, legend and axis can take + 145px before the plot gets any, so pinning a short frame and scaling the + graphic down shrinks the type below the app's own 11px chrome. The frame + grows to the chart instead, up to --chart-max-height. + + That cap is a length, not a percentage, and is repeated on the SVG below. + A percentage `max-height` resolves against the parent's height, and this + parent has none to give — so the SVG would render at full height, the frame + would stop at its cap, and the difference would become a scrollbar. */ .chart { background: var(--paper); padding: 6px; display: flex; width: 100%; max-width: 600px; - height: 75vw; - max-height: 360px; + min-height: 220px; + /* Content box + the 6px padding either side (box-sizing: border-box). */ + max-height: calc(var(--chart-max-height) + 12px); margin-inline: auto; justify-content: center; align-items: center; overflow: auto; } +/* The SVG is the flex item; this wrapper only carries the markup. */ +.chart-svg { + display: contents; +} + +.chart-pending { + color: var(--muted); +} + .chart svg { display: block; width: auto; height: auto; max-width: 100%; - max-height: 100%; + max-height: var(--chart-max-height); flex: none; } -.placeholder { - border: 1px solid var(--hairline); - padding: 40px; - text-align: center; - color: var(--muted); -} .error { border: 1px solid var(--hairline); From 584bd64e3712334c935e52208fbb3b7d417ae7c7 Mon Sep 17 00:00:00 2001 From: Chenglong Wang Date: Mon, 3 Aug 2026 13:09:52 -0700 Subject: [PATCH 087/164] theme: every house says how big its dots are MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A scatter dot had no owner. Six of the nine houses never named a size, so their dots came out at the renderer's own 30px² — nobody's design decision — and, because the crowding budget can only cut a size that exists, those houses also opted out of ever giving ground when the plot filled up. At 1,200 points they still drew 6px dots covering a fifth of the plot. So each house now names a size, and the ladder is the house's own: 45 for nature, whose journal panels are small and whose symbols are meant to be read not seen; 48 datawrapper, 52 swiss, 58 nyt, 62 powerbi, 66 economist; 72 for mckinsey, whose charts are read across a room; 170 for cartoon's sticker. The cross-library convention is a 6px dot, but that is the size of a dot in a *typical* plot — it is the answer to a question about density, and stating it as a constant is what made the sparse plots look weedy. A house's size is the size of a dot that has room; crowding takes it from there. Two things had to be fixed before the budget could be trusted with more weight: - It charged one panel for the whole table. A six-panel facet draws a sixth of the rows in each panel, so every dot shrank to make room for five panels' worth of marks that were not drawn there. - It charged charts that draw no dot cloud at all. A boxplot's points are its outliers and a line's are its vertices; neither is one per row, and neither should pay the crowd's rent. The floor drops from 36px² to 20px². 36 sat above the renderer's own default, so the budget could never actually bite — nature's dense scatter was inked over 38% of its plot when the budget said 12%. 20px² is about 5px across, which is where a dot stops being a dot: past there the answer is not a smaller dot but a different chart. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 175e0e03-5dda-4a02-aa14-c181660b9c99 --- .../src/core/theme/presets/datawrapper.ts | 3 + .../src/core/theme/presets/economist.ts | 3 + .../src/core/theme/presets/mckinsey.ts | 2 +- .../flint-js/src/core/theme/presets/nyt.ts | 3 + .../src/core/theme/presets/powerbi-light.ts | 3 + .../src/core/theme/presets/powerbi.ts | 3 + .../flint-js/src/core/theme/presets/swiss.ts | 3 + packages/flint-js/src/vegalite/theme.ts | 83 ++++++++++++-- packages/flint-js/tests/point-size.test.ts | 105 ++++++++++++++++++ 9 files changed, 199 insertions(+), 9 deletions(-) create mode 100644 packages/flint-js/tests/point-size.test.ts diff --git a/packages/flint-js/src/core/theme/presets/datawrapper.ts b/packages/flint-js/src/core/theme/presets/datawrapper.ts index 00d4d497..b8697c58 100644 --- a/packages/flint-js/src/core/theme/presets/datawrapper.ts +++ b/packages/flint-js/src/core/theme/presets/datawrapper.ts @@ -132,6 +132,9 @@ export const datawrapper: ThemePreset = { "source": "surface", "width": 1.5 }, + "point": { + "size": 48 + }, "connector": { "presence": "full", "weight": 1 diff --git a/packages/flint-js/src/core/theme/presets/economist.ts b/packages/flint-js/src/core/theme/presets/economist.ts index 19ecbee2..f1634bcd 100644 --- a/packages/flint-js/src/core/theme/presets/economist.ts +++ b/packages/flint-js/src/core/theme/presets/economist.ts @@ -135,6 +135,9 @@ export const economist: ThemePreset = { "edge": "quiet", "inkSource": "sameAsCentral" }, + "point": { + "size": 66 + }, "sizeRange": [ 10, 450 diff --git a/packages/flint-js/src/core/theme/presets/mckinsey.ts b/packages/flint-js/src/core/theme/presets/mckinsey.ts index 299909d2..b6df40e7 100644 --- a/packages/flint-js/src/core/theme/presets/mckinsey.ts +++ b/packages/flint-js/src/core/theme/presets/mckinsey.ts @@ -161,7 +161,7 @@ export const mckinsey: ThemePreset = { "spanWeight": 3 }, "point": { - "size": 64 + "size": 72 }, "separator": { "presence": "hairline", diff --git a/packages/flint-js/src/core/theme/presets/nyt.ts b/packages/flint-js/src/core/theme/presets/nyt.ts index fca7e743..a759871f 100644 --- a/packages/flint-js/src/core/theme/presets/nyt.ts +++ b/packages/flint-js/src/core/theme/presets/nyt.ts @@ -157,6 +157,9 @@ export const nyt: ThemePreset = { "tile": { "gap": 1 }, + "point": { + "size": 58 + }, "zOrder": "summaryOverData", "redundantEncoding": "whenNeeded", "redundantChannels": [ diff --git a/packages/flint-js/src/core/theme/presets/powerbi-light.ts b/packages/flint-js/src/core/theme/presets/powerbi-light.ts index f7da5e3a..16de90e3 100644 --- a/packages/flint-js/src/core/theme/presets/powerbi-light.ts +++ b/packages/flint-js/src/core/theme/presets/powerbi-light.ts @@ -157,6 +157,9 @@ export const powerbiLight: ThemePreset = { "strokeWeight": 2.2, "strokeCap": "square", "minSize": 1.5, + "point": { + "size": 62 + }, "separator": { "presence": "hairline", "source": "surface", diff --git a/packages/flint-js/src/core/theme/presets/powerbi.ts b/packages/flint-js/src/core/theme/presets/powerbi.ts index d2108b05..666893bf 100644 --- a/packages/flint-js/src/core/theme/presets/powerbi.ts +++ b/packages/flint-js/src/core/theme/presets/powerbi.ts @@ -156,6 +156,9 @@ export const powerbi: ThemePreset = { "strokeWeight": 2.2, "strokeCap": "square", "minSize": 1.5, + "point": { + "size": 62 + }, "separator": { "presence": "hairline", "source": "surface", diff --git a/packages/flint-js/src/core/theme/presets/swiss.ts b/packages/flint-js/src/core/theme/presets/swiss.ts index e2abfe52..01f12182 100644 --- a/packages/flint-js/src/core/theme/presets/swiss.ts +++ b/packages/flint-js/src/core/theme/presets/swiss.ts @@ -152,6 +152,9 @@ export const swiss: ThemePreset = { point: { presence: 'omit', fill: 'solid', + // Small and exact. The grid does the work of placing a + // reading here, so the dot only has to mark the spot. + size: 52, }, separator: { presence: 'hairline', diff --git a/packages/flint-js/src/vegalite/theme.ts b/packages/flint-js/src/vegalite/theme.ts index 9e275162..d26a7b36 100644 --- a/packages/flint-js/src/vegalite/theme.ts +++ b/packages/flint-js/src/vegalite/theme.ts @@ -986,8 +986,58 @@ function protectDashEncoding(spec: any, config: any, strokeWidth: number): void }); } -function applyMarks(spec: any, d: DesignDecisions, table: any[], say: (p: string, m: string) => void): void { - const config = spec.config; +/** + * The area below which a dot stops reading as a dot — about 5px across. The + * crowding budget may shrink a house's dots this far and no further; past + * here the answer is no longer a smaller dot but a different chart. + */ +const MIN_DENSE_POINT_SIZE = 20; + +/** + * How many panels the table is spread across. A facet draws each panel from + * its own slice of the rows, so anything budgeting panel space against the + * row count has to divide by this first. + */ +function panelCount(spec: any, table: any[]): number { + const fields = new Set(); + const note = (def: any) => { if (def?.field) fields.add(def.field); }; + walk(spec, (node) => { + note(node.encoding?.facet); + note(node.encoding?.row); + note(node.encoding?.column); + note(node.facet); + note(node.facet?.row); + note(node.facet?.column); + }); + note(spec.facet); + note(spec.facet?.row); + note(spec.facet?.column); + if (fields.size === 0) return 1; + let panels = 1; + for (const field of fields) { + const values = new Set(table.map((r) => r?.[field]).filter((v) => v != null)); + panels *= Math.max(1, values.size); + } + return panels; +} + +/** + * Whether the spec draws a dot per row — a scatter, a strip, a dot plot. Only + * then does the crowding budget below have a claim on the plot's area: a + * boxplot's dots are its outliers and a line's are its vertices, and neither + * is one-per-row. + */ +function drawsPointCloud(spec: any): boolean { + let found = false; + walk(spec, (node) => { + if (found) return; + const type = markTypeOf(node.mark); + if (type === 'point' || type === 'circle' || type === 'square') found = true; + }); + return found; +} + +function applyMarks(spec: any, d: DesignDecisions, table: any[], say: (p: string, m: string) => void): void { const config = spec.config; const m = d.marks; const plotWidth = spec.config?.view?.continuousWidth ?? spec._width ?? 300; const plotHeight = spec.config?.view?.continuousHeight ?? spec._height ?? 300; @@ -1149,11 +1199,28 @@ function applyMarks(spec: any, d: DesignDecisions, table: any[], say: (p: string // a house that sized only `point` silently missed every scatter it had. if (m.point?.size != null || m.point?.filled != null) { // This config reaches only standalone point-family marks; line - // vertices use `config.line.point` above. It is therefore safe to - // budget the standalone dots from the row count even when a template - // inherits its mark through a facet or composition node. - const densitySize = table.length > 0 - ? Math.max(36, Math.floor((plotWidth * plotHeight * 0.12) / table.length)) + // vertices use `config.line.point` above. + // + // The house's size is the size of a dot that has room. Crowd the plot + // and the dots have to give ground, or the reading stops being a + // cloud of observations and becomes one solid field. The budget is + // the share of the plot the dots may collectively ink — 12%, the + // middle of the 10-15% band the practitioner literature converges on + // — divided among the dots that have to share it. + // + // `plotWidth`/`plotHeight` are one panel, so the count has to be one + // panel's worth too: a six-panel facet draws a sixth of the table in + // each panel, and budgeting all of it against one panel would shrink + // every dot to the floor for rows that are not even drawn there. + // + // And the budget only means anything where the dots *are* the + // reading. A boxplot draws a dot per outlier, a line chart draws none + // at all: charging those few dots for every row in the table would + // shrink an outlier to a speck to make room for marks that were never + // drawn. + const perPanel = table.length / Math.max(1, panelCount(spec, table)); + const densitySize = table.length > 0 && drawsPointCloud(spec) + ? Math.max(MIN_DENSE_POINT_SIZE, Math.floor((plotWidth * plotHeight * 0.12) / perPanel)) : undefined; const pointSize = m.point.size != null && densitySize != null ? Math.min(m.point.size, densitySize) @@ -1172,7 +1239,7 @@ function applyMarks(spec: any, d: DesignDecisions, table: any[], say: (p: string if (m.point.size != null) { if (pointSize !== m.point.size) { say('marks.point.size', - `${table.length} points would cover too much of the ${Math.round(plotWidth)}×${Math.round(plotHeight)}px plot at ${m.point.size}px² each, so the dots shrink to ${pointSize}px²`); + `${Math.round(perPanel)} points would cover too much of the ${Math.round(plotWidth)}×${Math.round(plotHeight)}px plot at ${m.point.size}px² each, so the dots shrink to ${pointSize}px²`); } else { say('marks.point.size', `a dot is drawn at ${m.point.size}px² wherever one is drawn — the house's size, not the renderer's`); diff --git a/packages/flint-js/tests/point-size.test.ts b/packages/flint-js/tests/point-size.test.ts new file mode 100644 index 00000000..c4f2e18e --- /dev/null +++ b/packages/flint-js/tests/point-size.test.ts @@ -0,0 +1,105 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { describe, it, expect } from 'vitest'; +import { assembleVegaLite } from '../src'; +import { THEME_PRESETS } from '../src/core/theme/presets'; + +/** + * How big a dot is. + * + * A house's dot size is the size of a dot that has room. Crowd the plot and + * the dots have to give ground, or a cloud of observations turns into one + * solid field — but they may only give so much before a dot stops being a + * dot, and they must not give ground for marks that were never drawn. + */ + +const scatter = (n: number, house: string, extra: Record = {}, groups = 6) => { + let seed = 7; + const rnd = () => (seed = (seed * 1103515245 + 12345) % 2147483648) / 2147483648; + const values = Array.from({ length: n }, (_, i) => ({ + Weight: 1 + rnd() * 3, + Economy: 12 + rnd() * 30, + Group: `G${i % groups}`, + })); + return assembleVegaLite({ + data: { values }, + semantic_types: { Weight: 'Quantity', Economy: 'Quantity', Group: 'Category' }, + chart_spec: { + chartType: 'Scatter Plot', + encodings: { x: { field: 'Weight' }, y: { field: 'Economy' }, ...extra }, + }, + theme_spec: THEME_PRESETS[house].spec, + } as never) as never as { config?: Record }; +}; + +const drawnSize = (spec: { config?: Record }) => + spec.config?.point?.size ?? spec.config?.circle?.size; + +const declaredSize = (house: string) => + (THEME_PRESETS[house].spec as { marks?: { point?: { size?: number } } }).marks?.point?.size; + +describe('point size', () => { + it('every house says how big its dots are', () => { + // Silence is not a style. A house that never names a size inherits + // the renderer's own default, which is nobody's design decision, and + // — because the crowding budget can only cut a size that exists — + // it also opts out of ever giving ground when the plot fills up. + for (const id of Object.keys(THEME_PRESETS)) { + expect(declaredSize(id), `${id} declares no point size`).toBeGreaterThan(0); + } + }); + + it('a sparse scatter draws the house size', () => { + for (const id of ['economist', 'swiss', 'nyt', 'mckinsey']) { + const drawn = drawnSize(scatter(30, id)); + expect(drawn, `${id} draws no size of its own`).toBeGreaterThan(0); + expect(drawn, id).toBe(declaredSize(id)); + } + }); + + it('a crowded scatter gives ground, but never past the floor', () => { + for (const id of ['economist', 'swiss', 'nyt', 'mckinsey']) { + const dense = drawnSize(scatter(1500, id))!; + expect(dense, `${id} did not shrink`).toBeLessThan(declaredSize(id)!); + expect(dense, `${id} shrank past the floor`).toBeGreaterThanOrEqual(20); + } + }); + + it('a facet budgets each panel against its own rows', () => { + // Six panels draw a sixth of the table each. Charging one panel for + // the whole table would shrink every dot to the floor to make room + // for five panels' worth of marks that are not drawn there. + const rows = 300; + const panels = 3; + const spec = scatter(rows, 'mckinsey', { column: { field: 'Group' } }, panels); + const view = (spec.config as unknown as { view: { continuousWidth: number; continuousHeight: number } }).view; + const budget = (share: number) => Math.floor((view.continuousWidth * view.continuousHeight * 0.12) / share); + const perPanel = budget(rows / panels); + // The dots are charged for the rows their own panel draws, not for + // the whole table — which would be three times as harsh. + expect(budget(rows)).toBeLessThan(perPanel); + expect(drawnSize(spec)).toBe(Math.min(declaredSize('mckinsey')!, perPanel)); + }); + + it('a chart that draws no dot cloud keeps the house size', () => { + // A line chart's dots are its vertices and a boxplot's are its + // outliers — neither is one per row, so neither pays the crowd's + // rent for rows it never drew. + const values = Array.from({ length: 900 }, (_, i) => ({ + Month: `2024-${String((i % 12) + 1).padStart(2, '0')}-01`, + Revenue: 100 + (i % 37), + Series: `S${i % 3}`, + })); + const spec = assembleVegaLite({ + data: { values }, + semantic_types: { Month: 'Time', Revenue: 'Quantity', Series: 'Category' }, + chart_spec: { + chartType: 'Line Chart', + encodings: { x: { field: 'Month' }, y: { field: 'Revenue' }, color: { field: 'Series' } }, + }, + theme_spec: THEME_PRESETS['mckinsey'].spec, + } as never) as never as { config?: Record }; + expect(drawnSize(spec)).toBe(declaredSize('mckinsey')); + }); +}); From a0857dd7744c45a863c1460c213d58671e5417f6 Mon Sep 17 00:00:00 2001 From: Chenglong Wang Date: Mon, 3 Aug 2026 13:25:24 -0700 Subject: [PATCH 088/164] theme: a dot size that reaches only the dots it meant MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review of the previous commit turned up three ways the new sizes went somewhere they were not aimed. `config.point` is not only the point *mark*. Vega-Lite normalizes a line's `point: true` overlay into a symbol styled `point`, so a size written there also resized every line vertex in the chart — and a vertex's size is a question about the spacing along its line, not about the plot's area, so the crowding budget is the wrong rule for it and did not apply. A 120-reading connected scatter went from 6px vertices to 9px ones with nothing checking whether they fitted. The size now goes to `circle` and `square`, which no overlay borrows, and onto the point marks that really are a data cloud. A house that named only a size was also, by the mere existence of the `point` block, taken to have named a fill. Vega-Lite draws a bare `point` hollow, which is how a shape-encoded scatter tells its glyphs apart without colour; five houses' shape scatters silently turned solid. `filled` is now resolved only from an explicit `fill`, which is what the comment above it always said it did. And the budget was not the last word on size anyway. The scatter template runs its own coverage estimate at build time, against an assumed 400x300 plot and the whole table, and writes `mark.size` — which beats any config. So in exactly the dense case the budget exists for, the drawn dot was 13px², not the 20px² floor the budget had just worked out. The theme was given the real plot and knows how many panels the rows are spread over, so where both have an opinion the theme's now wins; a size a template fitted to a lane is left alone, since that measured something this pass cannot see. The three preset tests that asserted `config.point.size` now assert the size the dot is actually drawn at, which is what they meant and is the assertion that would have caught the third bug. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 175e0e03-5dda-4a02-aa14-c181660b9c99 --- packages/flint-js/src/core/theme/ground.ts | 12 +++-- .../flint-js/src/vegalite/templates/utils.ts | 11 ++++ packages/flint-js/src/vegalite/theme.ts | 32 ++++++++++-- packages/flint-js/tests/point-size.test.ts | 23 ++++++--- packages/flint-js/tests/theme-presets.test.ts | 51 +++++++++++++------ 5 files changed, 99 insertions(+), 30 deletions(-) diff --git a/packages/flint-js/src/core/theme/ground.ts b/packages/flint-js/src/core/theme/ground.ts index f3cf9d2c..3dfdfe9a 100644 --- a/packages/flint-js/src/core/theme/ground.ts +++ b/packages/flint-js/src/core/theme/ground.ts @@ -1174,10 +1174,14 @@ export function groundTheme(themeIn: ThemeSpec, ctx: GroundingContext): DesignDe ? { show: (marksSpec.point?.presence ?? 'omit') !== 'omit', size: marksSpec.point?.size, - // Only a house that spoke about its dots decides how they are - // filled; inventing an answer here would re-fill every - // scatter it has for the sake of a line chart's vertices. - filled: marksSpec.point ? marksSpec.point.fill !== 'hollow' : undefined, + // Only a house that spoke about its dots' fill decides how + // they are filled; inventing an answer here would re-fill + // every scatter it has for the sake of a line chart's + // vertices — or, for a house that named only a size, turn + // the hollow glyphs of a shape-encoded scatter solid. + filled: marksSpec.point?.fill != null + ? marksSpec.point.fill !== 'hollow' + : undefined, haloColor: halo ? plot : undefined, haloWidth: marksSpec.point?.halo?.width ?? (halo ? 1.5 : undefined), } diff --git a/packages/flint-js/src/vegalite/templates/utils.ts b/packages/flint-js/src/vegalite/templates/utils.ts index 7a0d353e..8e461699 100644 --- a/packages/flint-js/src/vegalite/templates/utils.ts +++ b/packages/flint-js/src/vegalite/templates/utils.ts @@ -79,6 +79,16 @@ export function setMarkProp(mark: any, key: string, value: any): any { return { ...mark, [key]: value }; } +/** + * Marks whose size came from the coarse coverage estimate in + * `applyPointSizeScaling`, which runs at build time against an assumed plot + * and the whole table. A theme knows the plot it actually got and how many + * panels the rows are spread over, so where both have an opinion the theme's + * is the better-informed one — but only for the marks this rule sized, never + * for a mark a template fitted to a lane. + */ +export const coverageSizedMarks = new WeakSet(); + /** * Coverage-based point sizing. */ @@ -107,6 +117,7 @@ export const applyPointSizeScaling = ( const size = Math.round(Math.max(minSize, (targetCoverage * plotArea) / n)); vgSpec.mark = setMarkProp(vgSpec.mark, 'size', size); + coverageSizedMarks.add(vgSpec.mark); return vgSpec; }; diff --git a/packages/flint-js/src/vegalite/theme.ts b/packages/flint-js/src/vegalite/theme.ts index d26a7b36..636f58b2 100644 --- a/packages/flint-js/src/vegalite/theme.ts +++ b/packages/flint-js/src/vegalite/theme.ts @@ -16,7 +16,7 @@ import type { DesignDecisions, ThemeReport } from '../core/theme/types.js'; import { contrastingInk, parseColor, luminance, toHex } from '../core/theme/presence.js'; -import { CONTINUOUS_BAR_STEP_FILL } from './templates/utils.js'; +import { CONTINUOUS_BAR_STEP_FILL, coverageSizedMarks } from './templates/utils.js'; import { LOCAL_DODGE_LANE_FILL } from './templates/bar.js'; import { CANVAS_FURNITURE_KEY, readCanvasFurniture, type CanvasFurnitureItem } from './canvas-furniture.js'; @@ -1231,11 +1231,37 @@ function applyMarks(spec: any, d: DesignDecisions, table: any[], say: (p: string for (const family of ['point', 'circle', 'square'] as const) { config[family] = { ...(config[family] ?? {}), - ...(pointSize != null ? { size: pointSize } : {}), - filled: m.point.filled !== false, + // `config.point` is not only the point *mark*: Vega-Lite + // normalizes a line's `point: true` overlay into a symbol + // styled `point`, so a size written here would also resize + // every line vertex in the chart — dots whose spacing along + // the line, not the plot's area, is what decides their size. + // Only `circle` and `square`, which no overlay borrows, can + // take the size from the config; the point marks that really + // are a data cloud are sized on the mark below. + ...(pointSize != null && family !== 'point' ? { size: pointSize } : {}), + // Only a house that spoke about its dots decides how they are + // filled. Vega-Lite draws a bare `point` hollow, which is how + // a shape-encoded scatter tells its glyphs apart, and a house + // that merely named a size has not asked for that to change. + ...(m.point.filled != null ? { filled: m.point.filled } : {}), ...(m.outline ? { stroke: m.outline.color, strokeWidth: pointOutlineWidth } : {}), }; } + if (pointSize != null) { + walk(spec, (node) => { + const type = markTypeOf(node.mark); + if (type !== 'point' && type !== 'circle' && type !== 'square') return; + const mark = normalizeMark(node.mark); + // A template that fitted its dot to a lane measured something + // this pass cannot see, so its size stands. A size left by + // the build-time coverage estimate does not: it assumed a + // plot rather than being given one, and knew nothing of + // facets, so the budget above supersedes it. + if (mark.size != null && !coverageSizedMarks.has(node.mark)) return; + node.mark = { ...mark, size: pointSize }; + }); + } if (m.point.size != null) { if (pointSize !== m.point.size) { say('marks.point.size', diff --git a/packages/flint-js/tests/point-size.test.ts b/packages/flint-js/tests/point-size.test.ts index c4f2e18e..92bc36d2 100644 --- a/packages/flint-js/tests/point-size.test.ts +++ b/packages/flint-js/tests/point-size.test.ts @@ -30,11 +30,15 @@ const scatter = (n: number, house: string, extra: Record = {}, encodings: { x: { field: 'Weight' }, y: { field: 'Economy' }, ...extra }, }, theme_spec: THEME_PRESETS[house].spec, - } as never) as never as { config?: Record }; + } as never) as never as { mark?: unknown; config?: Record }; }; -const drawnSize = (spec: { config?: Record }) => - spec.config?.point?.size ?? spec.config?.circle?.size; +const drawnSize = (spec: { mark?: unknown; config?: Record }) => { + // A mark-level size beats the config block, so reading the config alone + // can report a size that nothing is actually drawn at. + const mark = (typeof spec.mark === 'string' ? { type: spec.mark } : spec.mark ?? {}) as { type?: string; size?: number }; + return mark.size ?? (mark.type ? spec.config?.[mark.type]?.size : undefined); +}; const declaredSize = (house: string) => (THEME_PRESETS[house].spec as { marks?: { point?: { size?: number } } }).marks?.point?.size; @@ -82,16 +86,18 @@ describe('point size', () => { expect(drawnSize(spec)).toBe(Math.min(declaredSize('mckinsey')!, perPanel)); }); - it('a chart that draws no dot cloud keeps the house size', () => { + it('a chart that draws no dot cloud is not charged for the crowd', () => { // A line chart's dots are its vertices and a boxplot's are its // outliers — neither is one per row, so neither pays the crowd's - // rent for rows it never drew. - const values = Array.from({ length: 900 }, (_, i) => ({ + // rent for rows it never drew. The same row count in a scatter, + // where every row *is* a dot, does have to give ground. + const rows = 900; + const values = Array.from({ length: rows }, (_, i) => ({ Month: `2024-${String((i % 12) + 1).padStart(2, '0')}-01`, Revenue: 100 + (i % 37), Series: `S${i % 3}`, })); - const spec = assembleVegaLite({ + const line = assembleVegaLite({ data: { values }, semantic_types: { Month: 'Time', Revenue: 'Quantity', Series: 'Category' }, chart_spec: { @@ -100,6 +106,7 @@ describe('point size', () => { }, theme_spec: THEME_PRESETS['mckinsey'].spec, } as never) as never as { config?: Record }; - expect(drawnSize(spec)).toBe(declaredSize('mckinsey')); + expect(line.config?.circle?.size).toBe(declaredSize('mckinsey')); + expect(drawnSize(scatter(rows, 'mckinsey'))).toBeLessThan(declaredSize('mckinsey')!); }); }); diff --git a/packages/flint-js/tests/theme-presets.test.ts b/packages/flint-js/tests/theme-presets.test.ts index 71e7d018..a1fa47c4 100644 --- a/packages/flint-js/tests/theme-presets.test.ts +++ b/packages/flint-js/tests/theme-presets.test.ts @@ -50,6 +50,15 @@ function markOf(spec: any): any { return typeof node.mark === 'string' ? { type: node.mark } : node.mark; } +/** + * The size a dot is actually drawn at. A mark-level `size` beats the config + * block, so reading the config alone can report a size nothing is drawn at. + */ +function dotSize(spec: any): number | undefined { + const mark = markOf(spec); + return mark?.size ?? spec.config?.[mark?.type]?.size; +} + describe('naming a house Flint ships', () => { it('reads the same as passing that house in full', () => { const named = build('economist' as any); @@ -132,7 +141,7 @@ describe('cartoon mark character', () => { const spec = scatter(12); expect(spec.config.point.stroke).toBe('#2e2b28'); expect(spec.config.point.strokeWidth).toBe(2.5); - expect(spec.config.point.size).toBe(170); + expect(dotSize(spec)).toBe(170); const line = build(THEME_PRESETS.cartoon.spec); expect(line.config.line.point.stroke).toBe('#2e2b28'); @@ -151,8 +160,8 @@ describe('cartoon mark character', () => { it('shrinks a dense point cloud without flattening sparse dots', () => { const dense = scatter(500); - expect(dense.config.point.size).toBeLessThan(170); - expect(dense.config.point.size).toBeGreaterThanOrEqual(36); + expect(dotSize(dense)).toBeLessThan(170); + expect(dotSize(dense)).toBeGreaterThanOrEqual(20); expect(dense.config.point.strokeWidth).toBeLessThan(2.5); expect(JSON.stringify(dense._theme?.report ?? [])).toContain('cover too much'); }); @@ -754,20 +763,32 @@ describe('what a connector joins, and what a dot has to carry alone', () => { }); it('sizes a dot the same wherever one is drawn', () => { - const rows = Array.from({ length: 12 }, (_, i) => ({ HP: 40 + i * 15, MPG: 40 - i * 2 })); - const spec = assembleVegaLite({ + const rows = Array.from({ length: 12 }, (_, i) => ({ HP: 40 + i * 15, MPG: 40 - i * 2, Kind: `K${i % 3}` })); + const houseSpec = theme({ + marks: { point: { presence: 'full', size: 45 } }, + } as Partial); + const at = (encodings: Record) => assembleVegaLite({ data: { values: rows }, - semantic_types: { HP: 'Quantity', MPG: 'Quantity' }, - chart_spec: { chartType: 'Scatter Plot', encodings: { x: 'HP', y: 'MPG' } }, - theme_spec: theme({ - marks: { point: { presence: 'full', size: 45 } }, - } as Partial), + semantic_types: { HP: 'Quantity', MPG: 'Quantity', Kind: 'Category' }, + chart_spec: { chartType: 'Scatter Plot', encodings }, + theme_spec: houseSpec, } as any) as any; - // The scatter is drawn as `circle`, which has its own config block — - // sizing `point` alone would have missed it entirely. - expect(spec.config.circle.size).toBe(45); - expect(spec.config.point.size).toBe(45); - expect(spec.config.line.point.size).toBe(45); + + // The plain scatter is drawn as `circle`, which has its own config + // block — sizing `point` alone would have missed it entirely. + const plain = at({ x: 'HP', y: 'MPG' }); + expect(markTypeOf(plain.mark)).toBe('circle'); + expect(dotSize(plain)).toBe(45); + + // A shape encoding promotes the same scatter to the `point` mark. It + // takes the house's size too — but from the mark, because + // `config.point` is shared with the symbol Vega-Lite draws for a + // line's vertices, whose size is a question about spacing. + const shaped = at({ x: 'HP', y: 'MPG', shape: { field: 'Kind' } }); + expect(markTypeOf(shaped.mark)).toBe('point'); + expect(dotSize(shaped)).toBe(45); + + expect(plain.config.line.point.size).toBe(45); }); }); From 8682029de24abb78bdcbc7efd8b3838e8bd93a39 Mon Sep 17 00:00:00 2001 From: Chenglong Wang Date: Mon, 3 Aug 2026 13:37:23 -0700 Subject: [PATCH 089/164] site: one style-references page instead of two identical labs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Swiss lab and the cartoon lab were the same page twice — a header and a grid of 400px cards, differing only in their prose and their case list. Adding a third house meant a third copy. There is now one page driven by a registry, so a house is an entry rather than a file: it declares its label, what its references argue for, and where the look was taken from. The specs themselves stay in their own files, which are design documents in their own right, and now share one case type instead of declaring the same four fields twice. The house lives in the URL, since linking to a specific reference is what these pages are for. The old /swiss-lab and /cartoon-lab paths redirect rather than break, and the nav group's active check now matches the page instead of the end of the path, which a nested house segment defeated. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 175e0e03-5dda-4a02-aa14-c181660b9c99 --- site/src/main.tsx | 10 ++- site/src/playground/CartoonLab.tsx | 75 ----------------- site/src/playground/PlaygroundShell.tsx | 7 +- site/src/playground/StyleReferences.tsx | 102 ++++++++++++++++++++++++ site/src/playground/SwissLab.tsx | 73 ----------------- site/src/playground/cartoon-lab-data.ts | 12 +-- site/src/playground/style-references.ts | 67 ++++++++++++++++ site/src/playground/swiss-lab-data.ts | 12 +-- 8 files changed, 187 insertions(+), 171 deletions(-) delete mode 100644 site/src/playground/CartoonLab.tsx create mode 100644 site/src/playground/StyleReferences.tsx delete mode 100644 site/src/playground/SwissLab.tsx create mode 100644 site/src/playground/style-references.ts diff --git a/site/src/main.tsx b/site/src/main.tsx index 80a5a7fa..4d961bb8 100644 --- a/site/src/main.tsx +++ b/site/src/main.tsx @@ -18,8 +18,7 @@ import { DemoWall } from './playground/DemoWall'; import { ThemeLab } from './playground/ThemeLab'; import { ThemeLabR2 } from './playground/ThemeLabR2'; import { ThemeLabReal } from './playground/ThemeLabReal'; -import { SwissLab } from './playground/SwissLab'; -import { CartoonLab } from './playground/CartoonLab'; +import { StyleReferences } from './playground/StyleReferences'; import { FullTestCases } from './playground/FullTestCases'; import { LocaleProvider, useLocale } from './i18n/LocaleContext'; import type { Locale } from './i18n/locales'; @@ -60,8 +59,11 @@ function AppRoutes({ locale }: { locale: Locale }) { } /> } /> } /> - } /> - } /> + } /> + {/* The Swiss and cartoon labs were the same page twice; keep the + links they were reached by working. */} + } /> + } /> } /> {/* Tutorials merged into Documentation as the "Quick start" group. */} diff --git a/site/src/playground/CartoonLab.tsx b/site/src/playground/CartoonLab.tsx deleted file mode 100644 index 788ff40e..00000000 --- a/site/src/playground/CartoonLab.tsx +++ /dev/null @@ -1,75 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -/** - * Cartoon lab — hand-authored Vega-Lite mockups of a playful "cartoon" look, - * laid out as a simple chart grid for look-and-feel inspection. These are - * manual reference specs (see cartoon-lab-data.ts), NOT theme-pipeline output. - * They are the design reference for the shipped `cartoon` ThemeSpec preset and - * remain useful for checking which parts of that target the pipeline can own. - */ - -import { siteTheme } from '../shared/theme'; -import { VegaLiteView } from '../components/VegaLiteView'; -import { CARTOON_CASES } from './cartoon-lab-data'; - -export function CartoonLab() { - return ( -
-
-

- Cartoon lab · hand-authored mockups -

-

- Manual Vega-Lite specs exploring a playful, xkcd-flavoured look — a rounded - comic face, a bright crayon palette on warm paper, fat dark "sticker" outlines - on rounded marks, big bordered dots, and emoji markers. These are the design - reference for the shipped cartoon preset, not theme-pipeline - output. The preset now owns the reusable levers; emoji markers remain an - intentionally hand-authored idea outside the theme spec. -

-
- -
- {CARTOON_CASES.map((c) => ( -
-
- -
-
-
- {c.title} - - {c.id} - -
-
{c.note}
-
-
- ))} -
-
- ); -} diff --git a/site/src/playground/PlaygroundShell.tsx b/site/src/playground/PlaygroundShell.tsx index 5ad0ca07..b8e09664 100644 --- a/site/src/playground/PlaygroundShell.tsx +++ b/site/src/playground/PlaygroundShell.tsx @@ -16,8 +16,7 @@ const pages: NavEntry[] = [ { to: 'theme-labs', label: 'Theme lab' }, { to: 'theme-lab-r2', label: 'Theme lab R2' }, { to: 'theme-lab-real', label: 'Theme lab real' }, - { to: 'swiss-lab', label: 'Swiss lab' }, - { to: 'cartoon-lab', label: 'Cartoon lab' }, + { to: 'style-references', label: 'Style references' }, ], }, { to: 'full-test-cases', label: 'Full test cases' }, @@ -25,7 +24,9 @@ const pages: NavEntry[] = [ function ThemeLabsMenu({ group, children }: { group: string; children: NavLeaf[] }) { const { pathname } = useLocation(); - const active = children.some((c) => pathname.endsWith(`/${c.to}`)); + // A page may carry further segments (style-references/swiss), so match the + // page rather than the end of the path. + const active = children.some((c) => pathname.includes(`/${c.to}`)); return (
-

- Every tile is a pair: on the left what Flint compiles today, on the right a Vega-Lite - spec written by hand against a named design language — no theming engine involved. - The wall is for scanning; click any tile for the full-size pair, the diff (the - concrete list of things a design-theme layer would have to be able to express) and - both raw specs. Tiles are grouped by chart, so a case themed in several languages - sits together. The Flint baseline is identical across a group, so it is drawn once, - on the first tile; the follow-ups pair the redesign against the compiled theme only. - Specs live in site/src/playground/theme-lab-assets/, one - JSON per chart per theme, tagged with __theme__. -

-

- Scope. Every redesign is reachable - from the same data and the same encoding. No annotation layers, no callouts, no - editorial headlines that state a conclusion, no invented reference lines, no source - credits. Those are worth studying later, but they need information the chart spec - does not carry. Nor may a redesign re-sort: which country leads, which age band sits - on top, which band rests on the baseline are statements about the data, decided - upstream in the Flint spec — where the baseline's order is wrong it is fixed there, - so both columns move together. What is in scope: geometry, scales, axis and grid - structure, palette, typography, legend placement, and re-encoding data that is - already present (for example printing a bar's value as a label). -

-

- Text is held constant. Both columns - carry byte-identical title and subtitle strings, defined once in{' '} - _headlines.json. Flint's ChartAssemblyInput has no title - field at all, so the baseline is post-processed to stamp the headline on unstyled — - otherwise column 2 would win simply by having words on the page. The difference you - are looking at is typography, anchoring, spacing and colour, never wording.{' '} - scripts/check-theme-lab.mjs fails the build if the two ever drift apart. -

@@ -830,7 +778,7 @@ export function ThemeLab() { marginBottom: 10, }} > - The {THEME_ORDER.length} design languages — palette, intent, signature + Palettes
-

- {t.intent} -

-
    - {t.signature.map((s, i) => ( -
  • {s}
  • - ))} -
); })} @@ -913,168 +853,11 @@ export function ThemeLab() {

)} -
); } -/** - * What the table covers, what it does not, and which additional cases would - * actually buy new information about the design space. - */ -function CoverageNotes({ rows }: { rows: LabRow[] }) { - const index = CASE_INDEX; - const done = new Set(rows.map((r) => r.id)); - const coveredTypes = new Set(rows.map((r) => r.chartType)); - const untouched = index.filter((e) => !done.has(e.id)); - const untouchedNew = untouched.filter((e) => !coveredTypes.has(e.chartType)); - const themesPerCase = new Map(); - rows.forEach((r) => themesPerCase.set(r.id, (themesPerCase.get(r.id) ?? 0) + 1)); - const full = [...themesPerCase.entries()].filter(([, n]) => n >= THEME_ORDER.length); - const once = [...themesPerCase.values()].filter((n) => n === 1).length; - - const bulletList: React.CSSProperties = { - margin: '6px 0 18px', - paddingLeft: 18, - maxWidth: 860, - fontSize: 13, - lineHeight: 1.6, - color: siteTheme.textMuted, - }; - const heading: React.CSSProperties = { - fontSize: 13.5, - fontWeight: 600, - color: siteTheme.text, - margin: '18px 0 0', - }; - - return ( -
-

- Coverage, and what is still missing -

-

- {rows.length} redesigns across {coveredTypes.size} chart types and {THEME_ORDER.length}{' '} - design languages, drawn from {index.length} Flint baselines. A case earns its place by - forcing a decision no theme here has had to make yet — more charts is not the same as more - evidence. -

- -

The biggest gap is not a missing chart

-
    -
  • - {full.length === 0 - ? `No case carries all ${THEME_ORDER.length} languages, and ${once} of ${themesPerCase.size} are themed exactly once.` - : `${full.length} of ${themesPerCase.size} cases carry all ${THEME_ORDER.length} languages (${full - .map(([id]) => id) - .join(', ')}); ${once} are themed exactly once.`}{' '} - Themed once, a row cannot separate what the language decided from what the chart - demanded. -
  • -
  • - Print the values or keep the axis is the chart talking, not the house: on the - bar most houses print the number on the mark and the Economist and Nature trust the - axis instead (Power BI, in either mode, keeps both); on the pie there is no axis to - trust, so everyone prints. -
  • -
  • - Legend or direct labels is the house talking, and it holds across charts: NYT, - the Economist and McKinsey label directly on both the line and the pie; Nature, - Datawrapper and Power BI (light and dark) keep a key on both. -
  • -
  • - So what a theme layer has to encode is an attitude to scaffolding, not a rule about - marks. Nature keeps the most, and is the only language that answers a problem by adding - an encoding rather than removing one; McKinsey keeps the least. -
  • -
- -

Missing: structural situations

-
    -
  • - Dual-axis / combo. Two units in one frame is the hardest thing to theme — which axis - keeps the grid, which series takes the accent. Unreachable: Flint has no Vega-Lite - combo template, so there is no baseline to argue against. -
  • -
  • - Missing data. No dataset here has a hole in it. Break the line, interpolate, or shade - the gap — a newspaper and a journal answer differently. -
  • -
  • - High cardinality in colour. The fifty-state row tests fifty axis labels, which is - typography. It never tests what happens when hue itself runs out. -
  • -
- -

Missing: chart types

-
    -
  • - Untouched baselines: {untouched.length}, but only {untouchedNew.length} add a chart - type never themed here ({untouchedNew.map((e) => e.chartType).join(', ')}). The other{' '} - {untouched.length - untouchedNew.length} are repeats of covered types and buy nothing — - a second scatter tests the same decisions as the first. -
  • -
  • Calendar heatmaps and cycle plots — seasonality is a layout question no case here asks.
  • -
  • - Point-symbol maps. The choropleth settles class breaks and hue ramps; a size legend - floating over a basemap is untested. -
  • -
  • - Radial forms — donut, rose, radar sit unpaired. Angle and area are the hardest channels - for a language to legislate, and the pie is the only one of the four any house here has - had to argue. -
  • -
  • Funnel and gauge — Plotly-only in Flint, so the baseline column cannot be produced at all.
  • -
- -

Missing: theme languages

-
    -
  • - The {THEME_ORDER.length} here are six houses plus a light mode of one — the languages - below would each force a decision no current house makes, not just widen the table. -
  • -
  • - Print-mono — one ink, texture and weight only. Every rule that currently leans on hue - would have to be restated. -
  • -
  • High-density terminal — tiny type, dark ground, no whitespace, information over legibility.
  • -
  • - Accessibility-first — an explicit contrast floor and pattern fills, which would collide - productively with the Datawrapper rows. -
  • -
  • - Raw exploratory — deliberately unstyled and disposable. The null hypothesis: the point - below which theming is not worth doing. -
  • -
- -
- - Baselines with no bespoke counterpart yet ({untouched.length}) - -
- {untouched.map((e) => ( - - {e.id}{' '} - · {e.chartType} - - ))} -
-
-
- ); -} - function filterBtn(active: boolean): React.CSSProperties { return { fontSize: 12, diff --git a/site/src/playground/ThemeLabR2.tsx b/site/src/playground/ThemeLabR2.tsx index 0f65a05b..455b7aaf 100644 --- a/site/src/playground/ThemeLabR2.tsx +++ b/site/src/playground/ThemeLabR2.tsx @@ -88,10 +88,6 @@ export function ThemeLabR2() {

Theme lab · round 2 (coverage)

-

- Gallery cases the compiler was never tuned against, each read as all six houses. - Held-out criterion: nothing broken, illegible or absurd, and the house present. -