Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions packages/flint-js/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,11 @@
"import": "./dist/excel/index.js",
"require": "./dist/excel/index.cjs"
},
"./image-charts": {
"types": "./dist/image-charts/index.d.ts",
"import": "./dist/image-charts/index.js",
"require": "./dist/image-charts/index.cjs"
},
"./test-data": {
"types": "./dist/test-data/index.d.ts",
"import": "./dist/test-data/index.js",
Expand Down
362 changes: 362 additions & 0 deletions packages/flint-js/src/image-charts/assemble.ts

Large diffs are not rendered by default.

49 changes: 49 additions & 0 deletions packages/flint-js/src/image-charts/chart-types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

/**
* Image-Charts chart-type mapping.
*
* Image-Charts renders through a fixed set of `cht` chart codes (the Google
* Image Charts / Image-Charts query grammar), so a Flint chart type maps to the
* closest native `cht`. Orientation (vertical vs horizontal) is decided by the
* assembler from channel semantics and selects the `bv*` vs `bh*` family.
*
* Coverage is partial by design (like the Excel backend): only chart types with
* a faithful `cht` equivalent are mapped. Everything else throws in `assemble`.
*/

/** Which Image-Charts `cht` family a Flint chart type maps to. */
export interface ImageChartsTypeMapping {
/** Base Image-Charts `cht` value (vertical / category-on-x orientation). */
cht: string;
/** `cht` for the horizontal (category-on-y) variant, when supported. */
horizontal?: string;
/** True for pie/doughnut charts: slice labels, no value/category axes. */
noAxes?: boolean;
/** True for XY (both-measure) scatter charts rendered as `lxy`. */
xy?: boolean;
/** True for radar charts, which use the `chxt=r` polar axis. */
radar?: boolean;
/** True for area charts: a line (`lc`) plus a `chm=B` fill to the baseline. */
area?: boolean;
}

/** Flint chart type (display name) → Image-Charts `cht` family. */
export const IMAGE_CHARTS_TYPE_MAP: Record<string, ImageChartsTypeMapping> = {
'Bar Chart': { cht: 'bvg', horizontal: 'bhg' },
'Grouped Bar Chart': { cht: 'bvg', horizontal: 'bhg' },
'Stacked Bar Chart': { cht: 'bvs', horizontal: 'bhs' },
'Line Chart': { cht: 'lc' },
'Sparkline': { cht: 'ls' },
'Area Chart': { cht: 'lc', area: true },
'Scatter Plot': { cht: 'lxy', xy: true },
'Pie Chart': { cht: 'p', noAxes: true },
'Donut Chart': { cht: 'pd', noAxes: true },
'Radar Chart': { cht: 'r', radar: true },
};

/** Chart types this backend can render as an Image-Charts URL. */
export function isImageChartsSupported(flintChartType: string): boolean {
return flintChartType in IMAGE_CHARTS_TYPE_MAP;
}
28 changes: 28 additions & 0 deletions packages/flint-js/src/image-charts/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

/**
* @module flint-chart/image-charts
*
* Image-Charts backend for flint-chart.
*
* Compiles the core semantic layer into a single permanent
* `https://image-charts.com` chart URL (the Google Image Charts / Image-Charts
* query grammar). The URL renders server-side and embeds anywhere an `<img>`
* works — email, PDF, Slack, no-code tools — with no runtime JavaScript.
*
* Architecture contrast with the other backends:
* VL: encoding-channel spec — { encoding: { x, y }, mark }
* EC: series-based option — { series: [...], xAxis, yAxis }
* CJS: dataset-based config — { type, data: { labels, datasets } }
* Excel: range/matrix spec — { chartType, data: [[...]], axes }
* Image-Charts: hosted-image URL — { type: 'image-charts', url }
*
* `assembleImageCharts` is PURE: it builds a string, performs no network I/O and
* no signing, and emits unsigned free-tier URLs only.
*/

export { assembleImageCharts } from './assemble';
export type { ImageChartsArtifact } from './assemble';
export { IMAGE_CHARTS_TYPE_MAP, isImageChartsSupported } from './chart-types';
export type { ImageChartsTypeMapping } from './chart-types';
3 changes: 3 additions & 0 deletions packages/flint-js/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,6 @@ export * from './plotly';

// Excel backend: assembleExcel + Excel chart spec types
export * from './excel';

// Image-Charts backend: assembleImageCharts + hosted-image-URL artifact type
export * from './image-charts';
123 changes: 123 additions & 0 deletions packages/flint-js/src/test-data/image-charts-tests.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

/**
* Gallery generators for the Image-Charts backend.
*
* These cases exercise the URL-grammar paths the backend builds: a plain bar
* (`cht=bvg`, `chxl` categories), a multi-series grouped bar (`chco` + `chdl`
* legend), a line, a filled area (`chm=B`), a pie (per-slice `chl` + `chco`),
* and a scatter (`cht=lxy` + `chm=s` markers). The data is backend-agnostic —
* the gallery renders it through `assembleImageCharts`.
*/

import { Type } from './df-types';
import { TestCase, makeField, makeEncodingItem } from './types';

const CATEGORY_META = { type: Type.String, semanticType: 'Category', levels: [] as any[] };
const QUANTITY_META = { type: Type.Number, semanticType: 'Quantity', levels: [] as any[] };

export function genImageChartsTests(): TestCase[] {
return [
{
title: 'Bar — sales by region',
description: 'A single-series vertical bar, category labels on the x axis.',
tags: ['bar', 'nominal', 'quantitative', 'image-charts'],
chartType: 'Bar Chart',
data: [
{ Region: 'North', Sales: 42 },
{ Region: 'South', Sales: 35 },
{ Region: 'East', Sales: 58 },
{ Region: 'West', Sales: 27 },
],
fields: [makeField('Region'), makeField('Sales')],
metadata: { Region: CATEGORY_META, Sales: QUANTITY_META },
encodingMap: { x: makeEncodingItem('Region'), y: makeEncodingItem('Sales') },
},
{
title: 'Grouped bar — sales by region and channel',
description: 'Two series dodge per category, driving a per-series palette and a legend.',
tags: ['bar', 'grouped', 'series', 'legend', 'image-charts'],
chartType: 'Grouped Bar Chart',
data: [
{ Region: 'North', Sales: 42, Channel: 'Retail' },
{ Region: 'North', Sales: 20, Channel: 'Online' },
{ Region: 'South', Sales: 35, Channel: 'Retail' },
{ Region: 'South', Sales: 31, Channel: 'Online' },
],
fields: [makeField('Region'), makeField('Sales'), makeField('Channel')],
metadata: { Region: CATEGORY_META, Sales: QUANTITY_META, Channel: CATEGORY_META },
encodingMap: {
x: makeEncodingItem('Region'),
y: makeEncodingItem('Sales'),
group: makeEncodingItem('Channel'),
},
},
{
title: 'Line — monthly signups',
description: 'An ordered category axis with a single quantitative series.',
tags: ['line', 'temporal', 'quantitative', 'image-charts'],
chartType: 'Line Chart',
data: [
{ Month: '2026-01', Signups: 120 },
{ Month: '2026-02', Signups: 150 },
{ Month: '2026-03', Signups: 138 },
{ Month: '2026-04', Signups: 176 },
],
fields: [makeField('Month'), makeField('Signups')],
metadata: {
Month: { type: Type.String, semanticType: 'YearMonth', levels: [] },
Signups: QUANTITY_META,
},
encodingMap: { x: makeEncodingItem('Month'), y: makeEncodingItem('Signups') },
},
{
title: 'Area — traffic over time',
description: 'A line filled to the baseline via a chm=B marker.',
tags: ['area', 'temporal', 'quantitative', 'image-charts'],
chartType: 'Area Chart',
data: [
{ Day: '2026-01-01', Visits: 30 },
{ Day: '2026-01-02', Visits: 52 },
{ Day: '2026-01-03', Visits: 41 },
{ Day: '2026-01-04', Visits: 66 },
],
fields: [makeField('Day'), makeField('Visits')],
metadata: {
Day: { type: Type.Date, semanticType: 'Date', levels: [] },
Visits: QUANTITY_META,
},
encodingMap: { x: makeEncodingItem('Day'), y: makeEncodingItem('Visits') },
},
{
title: 'Pie — market share',
description: 'Slice labels and a per-slice palette.',
tags: ['pie', 'part-to-whole', 'image-charts'],
chartType: 'Pie Chart',
data: [
{ Vendor: 'Acme', Share: 45 },
{ Vendor: 'Globex', Share: 30 },
{ Vendor: 'Initech', Share: 15 },
{ Vendor: 'Umbrella', Share: 10 },
],
fields: [makeField('Vendor'), makeField('Share')],
metadata: { Vendor: CATEGORY_META, Share: QUANTITY_META },
encodingMap: { color: makeEncodingItem('Vendor'), size: makeEncodingItem('Share') },
},
{
title: 'Scatter — weight vs mpg',
description: 'Two measures on lxy, drawn as chm=s point markers.',
tags: ['scatter', 'quantitative', 'image-charts'],
chartType: 'Scatter Plot',
data: [
{ Weight: 1.6, Mpg: 32 },
{ Weight: 2.1, Mpg: 27 },
{ Weight: 1.9, Mpg: 29 },
{ Weight: 2.4, Mpg: 24 },
],
fields: [makeField('Weight'), makeField('Mpg')],
metadata: { Weight: QUANTITY_META, Mpg: QUANTITY_META },
encodingMap: { x: makeEncodingItem('Weight'), y: makeEncodingItem('Mpg') },
},
];
}
3 changes: 3 additions & 0 deletions packages/flint-js/src/test-data/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export { genLineAreaStretchTests } from './line-area-stretch-tests';
export { genEChartsScatterTests, genEChartsLineTests, genEChartsBarTests, genEChartsStackedBarTests, genEChartsGroupedBarTests, genEChartsStressTests, genEChartsAreaTests, genEChartsPieTests, genEChartsHeatmapTests, genEChartsHistogramTests, genEChartsBoxplotTests, genEChartsRadarTests, genEChartsCandlestickTests, genEChartsStreamgraphTests, genEChartsFacetSmallTests, genEChartsFacetWrapTests, genEChartsFacetClipTests, genEChartsRoseTests, genEChartsGaugeTests, genEChartsFunnelTests, genEChartsTreemapTests, genEChartsSunburstTests, genEChartsSankeyTests, genEChartsUniqueStressTests, genEChartsCalendarTests, genEChartsParallelTests, genEChartsGraphTests, genEChartsTreeTests } from './echarts-tests';
export { genChartJsScatterTests, genChartJsLineTests, genChartJsBarTests, genChartJsStackedBarTests, genChartJsGroupedBarTests, genChartJsAreaTests, genChartJsPieTests, genChartJsHistogramTests, genChartJsRadarTests, genChartJsStressTests, genChartJsRoseTests, genChartJsBubbleTests, genChartJsDoughnutTests, genChartJsComboTests } from './chartjs-tests';
export { genPlotlyCoreTests, genPlotlyFacetTests } from './plotly-tests';
export { genImageChartsTests } from './image-charts-tests';
export { genDiscreteAxisTests } from './discrete-axis-tests';
export { genDateTests, genDateYearTests, genDateMonthTests, genDateYearMonthTests, genDateDecadeTests, genDateDateTimeTests, genDateHoursTests } from './date-tests';
export { genSemanticContextTests, genSnapToBoundTests } from './semantic-tests';
Expand Down Expand Up @@ -116,6 +117,7 @@ import { genSemanticContextTests, genSnapToBoundTests } from './semantic-tests';
import { genEChartsScatterTests, genEChartsLineTests, genEChartsBarTests, genEChartsStackedBarTests, genEChartsGroupedBarTests, genEChartsStressTests, genEChartsAreaTests, genEChartsPieTests, genEChartsHeatmapTests, genEChartsHistogramTests, genEChartsBoxplotTests, genEChartsRadarTests, genEChartsCandlestickTests, genEChartsStreamgraphTests, genEChartsFacetSmallTests, genEChartsFacetWrapTests, genEChartsFacetClipTests, genEChartsRoseTests, genEChartsGaugeTests, genEChartsFunnelTests, genEChartsTreemapTests, genEChartsSunburstTests, genEChartsSankeyTests, genEChartsUniqueStressTests, genEChartsCalendarTests, genEChartsParallelTests, genEChartsGraphTests, genEChartsTreeTests } from './echarts-tests';
import { genChartJsScatterTests, genChartJsLineTests, genChartJsBarTests, genChartJsStackedBarTests, genChartJsGroupedBarTests, genChartJsAreaTests, genChartJsPieTests, genChartJsHistogramTests, genChartJsRadarTests, genChartJsStressTests, genChartJsRoseTests, genChartJsBubbleTests, genChartJsDoughnutTests, genChartJsComboTests } from './chartjs-tests';
import { genPlotlyCoreTests, genPlotlyFacetTests } from './plotly-tests';
import { genImageChartsTests } from './image-charts-tests';
import {
genGalleryRegionalSurveyScatterTests,
genGalleryRegionalSurveyLineTests,
Expand Down Expand Up @@ -256,6 +258,7 @@ export const TEST_GENERATORS: Record<string, () => TestCase[]> = {
'Chart.js: Stress Tests': genChartJsStressTests,
'Plotly: Core Templates': genPlotlyCoreTests,
'Plotly: Facets': genPlotlyFacetTests,
'Image-Charts: Core Templates': genImageChartsTests,
'Gallery: Scatter': genGalleryRegionalSurveyScatterTests,
'Gallery: Line': genGalleryRegionalSurveyLineTests,
'Gallery: Bar': genGalleryRegionalSurveyBarTests,
Expand Down
36 changes: 36 additions & 0 deletions packages/flint-js/tests/smoke.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
assembleChartjs,
assemblePlotly,
assembleExcel,
assembleImageCharts,
} from '../src';

const DATA = [
Expand Down Expand Up @@ -98,6 +99,41 @@ describe('public API smoke', () => {
expect(spec.seriesBy).toBe('Columns');
});

it('assembleImageCharts returns a permanent free-tier Image-Charts URL', () => {
const artifact = assembleImageCharts({
data: { values: [
{ Category: 'A', Value: 10 },
{ Category: 'B', Value: 20 },
{ Category: 'C', Value: 15 },
] },
semantic_types: { Category: 'Category', Value: 'Quantity' },
chart_spec: {
chartType: 'Bar Chart',
encodings: { x: 'Category', y: 'Value' },
title: 'Sales by region',
},
});

expect(artifact.type).toBe('image-charts');
expect(artifact.url.startsWith('https://image-charts.com/chart?')).toBe(true);
expect(artifact.url).toContain('cht=bvg');
expect(artifact.url).toContain('chd=a:10,20,15');
expect(artifact.url).toContain('chxl=0:|A|B|C');
expect(artifact.url).toContain('chtt=Sales+by+region');
// Free tier only: never signed, never an output override.
expect(artifact.url).not.toContain('icac');
expect(artifact.url).not.toContain('ichm');
expect(artifact.url).not.toContain('chof');
});

it('assembleImageCharts throws on chart types with no faithful cht', () => {
expect(() => assembleImageCharts({
data: { values: [{ Group: 'A', Value: 1 }, { Group: 'A', Value: 5 }] },
semantic_types: { Group: 'Category', Value: 'Quantity' },
chart_spec: { chartType: 'Boxplot', encodings: { x: 'Group', y: 'Value' } },
})).toThrow('does not support chart type "Boxplot"');
});

it('assembleExcel uses field display names for native axis titles', () => {
const spec = assembleExcel({
data: { values: [
Expand Down
1 change: 1 addition & 0 deletions packages/flint-js/tsup.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export default defineConfig({
'chartjs/index': 'src/chartjs/index.ts',
'plotly/index': 'src/plotly/index.ts',
'excel/index': 'src/excel/index.ts',
'image-charts/index': 'src/image-charts/index.ts',
'test-data/index': 'src/test-data/index.ts',
'gallery/index': 'src/gallery/index.ts',
},
Expand Down
Loading