Skip to content
Open
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
3 changes: 2 additions & 1 deletion agent-skills/flint-chart-author/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,7 @@ properties"). Required channels are noted.
| `"Boxplot"` | x, y, color, opacity, column, row | category + measure; props `whiskerMethod`, `showOutliers`, `dodge` |
| `"ECDF Plot"` | x, color, detail, column, row | x = measure; cumulative distribution (step line); prop `showPoints` |
| `"Heatmap"` | x, y, color, column, row | color = the measure |
| `"Calendar Heatmap"` | x, color | x = date; color = daily value (summed per day); GitHub-style week × weekday grid |
| `"Line Chart"` | x, y, color, strokeDash, detail, opacity, column, row | props `interpolate`, `showPoints` |
| `"Sparkline"` | x, y, color, detail, row, column | x + y required; small-multiple mini trend lines, one per series (series from `color` or `detail`); props `interpolate`, `baseline`, `trendWidth` |
| `"Bump Chart"` | x, y, color, detail, column, row | rank-over-time lines |
Expand Down Expand Up @@ -246,7 +247,7 @@ type column.
**Backend coverage.** Vega-Lite supports all of the above. Other backends
support a subset (verify if targeting a non-VL backend):

- **ECharts** adds: `"Calendar Heatmap"`, `"Gauge"`,
- **ECharts** adds: `"Gauge"`,
`"Funnel"`, `"Treemap"`, `"Sunburst"`, `"Sankey"`,
`"Parallel Coordinates"`, `"Graph"`, `"Tree"`.
- **Chart.js** supports: Scatter, Bubble, Bar, Grouped Bar, Stacked Bar,
Expand Down
8 changes: 7 additions & 1 deletion docs/reference-vegalite.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ The Vega-Lite backend serves as Flint's reference implementation and offers the

## What this page covers

This reference lists the 34 chart types currently supported by the Vega-Lite backend, grouped into 6 categories. Each chart entry shows:
This reference lists the 35 chart types currently supported by the Vega-Lite backend, grouped into 6 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.
Expand Down Expand Up @@ -384,6 +384,12 @@ _No template-specific parameters._
| `xAxisType` | choice | `temporal` (Temporal), `nominal` (Discrete) | — | conditional | Interpret the x-axis as a continuous time scale or discrete bands. |
| `yAxisType` | choice | `temporal` (Temporal), `nominal` (Discrete) | — | conditional | Interpret the y-axis as a continuous time scale or discrete bands. |

### ![](chart-icon-calendar.svg) Calendar Heatmap

**Encoding channels:** `x`, `color`

_No template-specific parameters._

### ![](chart-icon-bar-table.svg) Bar Table

**Encoding channels:** `y`, `x`, `color`, `column`, `row`
Expand Down
116 changes: 116 additions & 0 deletions packages/flint-js/src/vegalite/templates/calendar.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

/**
* Vega-Lite Calendar Heatmap template.
*
* The ECharts backend has a first-class calendar coordinate system; Vega-Lite
* has none, but it does not need computed week/day fields either — `timeUnit`
* expresses the GitHub-style grid directly from a single date field:
* x → timeUnit 'yearweek' (one ordinal column per calendar week)
* y → timeUnit 'day' (Sun–Sat, one ordinal row per weekday)
* so the same date field drives both axes and the sum-per-cell aggregation
* collapses to one value per calendar day.
*
* Encoding:
* x (temporal) → the date of each cell
* color (quantitative) → the cell value (defaults to a count of 1)
*/

import { ChartTemplateDef, EncodingActionDef } from '../../core/types';

/** Weekday row order, Monday-first — mirrors the ECharts template's dayLabel.firstDay = 1. */
const WEEKDAY_ORDER = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];

/**
* Sequential schemes. Named Vega-Lite schemes pass through as `scale.scheme`;
* 'github' has no built-in Vega-Lite equivalent, so it resolves to an explicit
* `scale.range` (the same low→high ramp the ECharts template uses).
*/
const GITHUB_RANGE = ['#ebedf0', '#9be9a8', '#40c463', '#30a14e', '#216e39'];
const VL_SCHEMES = new Set(['viridis', 'blues', 'greens', 'reds', 'oranges', 'purples']);

export const vlCalendarHeatmapDef: ChartTemplateDef = {
chart: 'Calendar Heatmap',
template: { mark: { type: 'rect', cornerRadius: 2 }, encoding: {} },
channels: ['x', 'color'],
markCognitiveChannel: 'color',
declareLayoutMode: () => ({
// Both axes are ordinal bands (week columns × weekday rows); square-ish
// cells read as a calendar rather than a stretched grid.
axisFlags: { x: { banded: true }, y: { banded: true } },
}),
instantiate: (spec, ctx) => {
const dateField = ctx.channelSemantics.x?.field;
const valueField = ctx.channelSemantics.color?.field;
if (!dateField) return;

const encScheme = ctx.encodings?.color?.scheme;
const scheme = encScheme && encScheme !== 'default' ? encScheme : 'viridis';
const colorScale =
scheme === 'github'
// Quantile scale snaps counts into the 5 canonical GitHub buckets
// (equal-count bins → discrete levels), rather than a smooth ramp.
? { type: 'quantile' as const, range: GITHUB_RANGE }
: { scheme: VL_SCHEMES.has(scheme) ? scheme : 'viridis' };

spec.encoding = {
// One ordinal column per calendar week; month initials label the axis.
x: {
field: dateField,
timeUnit: 'yearweek',
type: 'ordinal',
title: null,
axis: {
format: '%b',
labelAngle: 0,
labelOverlap: true,
tickBand: 'extent',
domain: false,
ticks: false,
},
},
// Sun–Sat rows, Monday-first to match the ECharts calendar.
y: {
field: dateField,
timeUnit: 'day',
type: 'ordinal',
title: null,
sort: WEEKDAY_ORDER,
axis: { domain: false, ticks: false },
},
// Sum collapses multiple rows sharing a calendar day into one cell.
color: {
...(valueField
? { field: valueField, aggregate: 'sum' }
: { aggregate: 'count' }),
type: 'quantitative',
legend: { title: null },
scale: colorScale,
},
};
},
encodingActions: [
{
key: 'colorScheme',
label: 'Scheme',
isApplicable: (ctx) => !!ctx.encodings.color?.field,
dependencies: ['color'],
control: {
type: 'discrete',
options: [
{ value: undefined, label: 'Default (Viridis)' },
{ value: 'viridis', label: 'Viridis' },
{ value: 'github', label: 'GitHub' },
{ value: 'blues', label: 'Blues' },
{ value: 'greens', label: 'Greens' },
{ value: 'reds', label: 'Reds' },
{ value: 'oranges', label: 'Oranges' },
{ value: 'purples', label: 'Purples' },
],
},
get: (enc) => enc.color?.scheme,
set: (enc, value) => ({ ...enc, color: { ...enc.color, scheme: value } }),
},
] as EncodingActionDef[],
};
3 changes: 2 additions & 1 deletion packages/flint-js/src/vegalite/templates/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import { radarChartDef } from './radar';
import { roseChartDef } from './rose';
import { mapDef, choroplethDef } from './map';
import { kpiCardDef } from './kpi-card';
import { vlCalendarHeatmapDef } from './calendar';

/**
* Cross-cutting properties injected into every template that supports
Expand Down Expand Up @@ -251,7 +252,7 @@ export const vlTemplateDefs: { [key: string]: ChartTemplateDef[] } = Object.from
"Distributions": [histogramDef, densityPlotDef, ecdfPlotDef, violinPlotDef, boxplotDef, pyramidChartDef, candlestickChartDef],
"Lines & Areas": [lineChartDef, sparklineDef, bumpChartDef, slopeChartDef, areaChartDef, streamgraphDef, rangeAreaChartDef],
"Circular": [pieChartDef, roseChartDef, radarChartDef],
"Tables & Maps": [heatmapDef, barTableDef, kpiCardDef, mapDef, choroplethDef],
"Tables & Maps": [heatmapDef, vlCalendarHeatmapDef, barTableDef, kpiCardDef, mapDef, choroplethDef],
}).map(([category, defs]) => [category, defs.map(withInjectedProperties)]),
);

Expand Down
98 changes: 98 additions & 0 deletions packages/flint-js/tests/calendar-vegalite.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

import { describe, it, expect } from 'vitest';
import { assembleVegaLite, assembleECharts, vlGetTemplateDef } from '../src';

/**
* Vega-Lite Calendar Heatmap — parity with the ECharts calendar template.
*
* Vega-Lite has no first-class calendar coordinate system, so the grid is
* expressed with `timeUnit`: the same date field drives `yearweek` (week
* columns) on x and `day` (weekday rows) on y, and `sum` collapses rows that
* share a calendar day into one cell.
*/

const DAILY = [
{ date: '2024-01-01', value: 5 },
{ date: '2024-01-02', value: 9 },
{ date: '2024-01-02', value: 1 }, // same day → summed with the row above
{ date: '2024-01-08', value: 12 },
{ date: '2024-02-05', value: 3 },
];

function calInput(extraEnc?: Record<string, unknown>) {
return {
data: { values: DAILY },
semantic_types: { date: 'Date', value: 'Amount' },
chart_spec: {
chartType: 'Calendar Heatmap',
encodings: { x: { field: 'date' }, color: { field: 'value', ...extraEnc } },
baseSize: { width: 420, height: 160 },
},
};
}

describe('Vega-Lite Calendar Heatmap', () => {
it('is registered in the Vega-Lite template registry', () => {
expect(vlGetTemplateDef('Calendar Heatmap')).toBeDefined();
});

it('drives both axes from the one date field via yearweek × day', () => {
const spec = assembleVegaLite(calInput()) as any;
expect(spec.mark?.type ?? spec.mark).toBe('rect');
expect(spec.encoding.x.field).toBe('date');
expect(spec.encoding.x.timeUnit).toBe('yearweek');
expect(spec.encoding.y.field).toBe('date');
expect(spec.encoding.y.timeUnit).toBe('day');
});

it('orders weekday rows Monday-first (matches the ECharts calendar)', () => {
const spec = assembleVegaLite(calInput()) as any;
expect(spec.encoding.y.sort).toEqual(['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']);
});

it('sums the value per calendar day', () => {
const spec = assembleVegaLite(calInput()) as any;
expect(spec.encoding.color.field).toBe('value');
expect(spec.encoding.color.aggregate).toBe('sum');
expect(spec.encoding.color.type).toBe('quantitative');
});

it('falls back to a per-day count when no value field is given', () => {
const input = {
data: { values: DAILY },
semantic_types: { date: 'Date' },
chart_spec: {
chartType: 'Calendar Heatmap',
encodings: { x: { field: 'date' } },
baseSize: { width: 420, height: 160 },
},
};
const spec = assembleVegaLite(input) as any;
expect(spec.encoding.color.aggregate).toBe('count');
expect(spec.encoding.color.field).toBeUndefined();
});

it('resolves the github scheme to a quantile scale over the canonical 5-bucket range', () => {
const spec = assembleVegaLite(calInput({ scheme: 'github' })) as any;
expect(spec.encoding.color.scale.scheme).toBeUndefined();
// Quantile scale → discrete GitHub buckets, not a continuous ramp.
expect(spec.encoding.color.scale.type).toBe('quantile');
expect(spec.encoding.color.scale.range).toEqual([
'#ebedf0', '#9be9a8', '#40c463', '#30a14e', '#216e39',
]);
});

it('passes a named scheme straight through to scale.scheme', () => {
const spec = assembleVegaLite(calInput({ scheme: 'blues' })) as any;
expect(spec.encoding.color.scale.scheme).toBe('blues');
});

it('assembles the same chart type on both backends (registry parity)', () => {
const vl = assembleVegaLite(calInput()) as any;
const ec = assembleECharts(calInput()) as any;
expect(vl.encoding.x.timeUnit).toBe('yearweek'); // VL: timeUnit grid
expect(ec.series?.[0]?.coordinateSystem).toBe('calendar'); // EC: calendar coord
});
});
3 changes: 2 additions & 1 deletion packages/flint-mcp/assets/flint-chart-author.SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,7 @@ properties"). Required channels are noted.
| `"Boxplot"` | x, y, color, opacity, column, row | category + measure; props `whiskerMethod`, `showOutliers`, `dodge` |
| `"ECDF Plot"` | x, color, detail, column, row | x = measure; cumulative distribution (step line); prop `showPoints` |
| `"Heatmap"` | x, y, color, column, row | color = the measure |
| `"Calendar Heatmap"` | x, color | x = date; color = daily value (summed per day); GitHub-style week × weekday grid |
| `"Line Chart"` | x, y, color, strokeDash, detail, opacity, column, row | props `interpolate`, `showPoints` |
| `"Sparkline"` | x, y, color, detail, row, column | x + y required; small-multiple mini trend lines, one per series (series from `color` or `detail`); props `interpolate`, `baseline`, `trendWidth` |
| `"Bump Chart"` | x, y, color, detail, column, row | rank-over-time lines |
Expand Down Expand Up @@ -246,7 +247,7 @@ type column.
**Backend coverage.** Vega-Lite supports all of the above. Other backends
support a subset (verify if targeting a non-VL backend):

- **ECharts** adds: `"Calendar Heatmap"`, `"Gauge"`,
- **ECharts** adds: `"Gauge"`,
`"Funnel"`, `"Treemap"`, `"Sunburst"`, `"Sankey"`,
`"Parallel Coordinates"`, `"Graph"`, `"Tree"`.
- **Chart.js** supports: Scatter, Bubble, Bar, Grouped Bar, Stacked Bar,
Expand Down