From 3ed55a35805cd93c384b0c88be8c8da829906eaf Mon Sep 17 00:00:00 2001 From: Chenglong Wang Date: Thu, 16 Jul 2026 20:34:32 -0700 Subject: [PATCH 1/9] recommendation fix --- packages/flint-js/src/chartjs/index.ts | 2 +- .../flint-js/src/chartjs/recommendation.ts | 40 +- .../src/core/chart-type-recommendation.ts | 373 ++++++++++++++++++ packages/flint-js/src/core/index.ts | 14 + packages/flint-js/src/core/recommendation.ts | 47 ++- packages/flint-js/src/echarts/index.ts | 2 +- .../flint-js/src/echarts/recommendation.ts | 40 +- packages/flint-js/src/vegalite/index.ts | 2 +- .../flint-js/src/vegalite/recommendation.ts | 53 ++- .../tests/chart-type-recommendation.test.ts | 211 ++++++++++ 10 files changed, 766 insertions(+), 18 deletions(-) create mode 100644 packages/flint-js/src/core/chart-type-recommendation.ts create mode 100644 packages/flint-js/tests/chart-type-recommendation.test.ts diff --git a/packages/flint-js/src/chartjs/index.ts b/packages/flint-js/src/chartjs/index.ts index 707dd2d7..1b3944dd 100644 --- a/packages/flint-js/src/chartjs/index.ts +++ b/packages/flint-js/src/chartjs/index.ts @@ -32,4 +32,4 @@ export { } from './templates'; // CJS recommendation & adaptation -export { cjsAdaptChart, cjsRecommendEncodings } from './recommendation'; +export { cjsAdaptChart, cjsRecommendEncodings, cjsRecommendChartTypes, cjsRecommendCharts } from './recommendation'; diff --git a/packages/flint-js/src/chartjs/recommendation.ts b/packages/flint-js/src/chartjs/recommendation.ts index 85b56487..c318a61d 100644 --- a/packages/flint-js/src/chartjs/recommendation.ts +++ b/packages/flint-js/src/chartjs/recommendation.ts @@ -6,7 +6,12 @@ */ import { adaptChannels, recommendChannels } from '../core/recommendation'; -import { cjsGetTemplateChannels } from './templates'; +import { + recommendChartTypes, + type RecommendChartTypesOptions, + type RecommendedChart, +} from '../core/chart-type-recommendation'; +import { cjsGetTemplateChannels, cjsAllTemplateDefs } from './templates'; export function cjsAdaptChart( sourceType: string, @@ -32,3 +37,36 @@ export function cjsRecommendEncodings( } return result; } + +/** Chart-type names Chart.js can render (its template catalog). */ +const CJS_SUPPORTED_TYPES = cjsAllTemplateDefs.map(d => d.chart); + +/** + * Recommend a ranked list of Chart.js chart types for a dataset, restricted to + * chart types Chart.js can render. See {@link cjsRecommendCharts} for the + * one-step variant that also fills channels. + */ +export function cjsRecommendChartTypes( + data: any[], + semanticTypes: Record, + options: Omit = {}, +): string[] { + return recommendChartTypes(data, semanticTypes, { ...options, supportedTypes: CJS_SUPPORTED_TYPES }); +} + +/** + * One-step recommendation: rank Chart.js chart types for the data, then populate + * each with {@link cjsRecommendEncodings}. Suggestions whose required channels + * cannot be filled are dropped, so every returned chart is renderable. + */ +export function cjsRecommendCharts( + data: any[], + semanticTypes: Record, + options: Omit = {}, +): RecommendedChart[] { + const { max, ...rest } = options; + const charts = cjsRecommendChartTypes(data, semanticTypes, rest) + .map(chartType => ({ chartType, encodings: cjsRecommendEncodings(chartType, data, semanticTypes) })) + .filter(s => Object.keys(s.encodings).length > 0); + return max != null ? charts.slice(0, max) : charts; +} diff --git a/packages/flint-js/src/core/chart-type-recommendation.ts b/packages/flint-js/src/core/chart-type-recommendation.ts new file mode 100644 index 00000000..858639a2 --- /dev/null +++ b/packages/flint-js/src/core/chart-type-recommendation.ts @@ -0,0 +1,373 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** + * ============================================================================= + * DATA-DRIVEN CHART *TYPE* RECOMMENDATION + * ============================================================================= + * + * `recommendChannels` / `getRecommendation` answer "which fields go on which + * channels **for a chart type I already picked**". They do not answer the step + * that sits *in front* of them: **which chart type should I use at all?** + * + * This module fills that gap. Given raw rows + semantic-type annotations it + * builds a deterministic {@link DataProfile} (how many measures / temporals / + * categoricals / geographic fields the table has, and their cardinalities) and + * scores a ranked list of candidate chart types. + * + * It reuses the same semantic-type classification the encoders use + * (`isMeasureType`, `isTimeSeriesType`, `isGeoType`, …) so the two stages agree + * on what a field *is*; it only adds the type-selection heuristic on top. + * + * Typical use (e.g. Data Formulator surfacing suggestions without a model call): + * + * ```ts + * const types = recommendChartTypes(rows, semanticTypes); // ["Choropleth", "Line Chart", …] + * const encodings = vlRecommendEncodings(types[0], rows, semanticTypes); + * ``` + * + * Backend wrappers (`vlRecommendChartTypes`, …) pass `supportedTypes` so only + * chart types that backend can actually render are returned, and pair it with + * their `…RecommendEncodings` to emit type + channels in one step + * (`…RecommendCharts`). + * + * ============================================================================= + */ + +import { + isMeasureType, + isTimeSeriesType, + isCategoricalType, + isOrdinalType, + isGeoCoordinateType, + isGeoLocationString, + isNonMeasureNumeric, +} from './semantic-types'; +import { buildTableView, nameMatches, type InternalTableView } from './recommendation'; + +// ── Field roles ───────────────────────────────────────────────────────── + +/** + * The role a field plays when choosing a chart type. Mutually exclusive: + * every field is classified into exactly one role (the most specific one). + */ +export type ChartFieldRole = + | 'measure' // true quantitative measure (aggregatable) + | 'temporal' // date / time-granule — a time axis + | 'categorical' // nominal category / entity + | 'ordinal' // ordered discrete (Rank, Range, Score, …) + | 'geoPlace' // named place: Country, State, City, Region … + | 'latitude' // geographic latitude coordinate + | 'longitude' // geographic longitude coordinate + | 'identifier' // row id / index — not useful as a category or measure + | 'other'; // unclassified + +/** A single field annotated with its data/semantic type, cardinality, and role. */ +export interface ProfiledField { + name: string; + /** Vega-Lite-style vis category: 'quantitative' | 'temporal' | 'nominal' | 'ordinal'. */ + type: string; + /** Semantic type annotation (e.g. "Country", "Amount"), or '' if none. */ + semanticType: string; + /** Number of distinct non-null values. */ + cardinality: number; + role: ChartFieldRole; +} + +/** + * A deterministic summary of a table's shape, used to rank chart types. + * The bucketed arrays are views over {@link fields} grouped by role, plus + * `dimensions` (all category-axis-capable fields: categorical ∪ ordinal ∪ + * geoPlace ∪ temporal). + */ +export interface DataProfile { + fields: ProfiledField[]; + measures: ProfiledField[]; + temporals: ProfiledField[]; + categoricals: ProfiledField[]; + ordinals: ProfiledField[]; + geoPlaces: ProfiledField[]; + latitudes: ProfiledField[]; + longitudes: ProfiledField[]; + identifiers: ProfiledField[]; + /** Fields usable on a category/discrete axis (categorical ∪ ordinal ∪ geoPlace ∪ temporal). */ + dimensions: ProfiledField[]; + rowCount: number; +} + +// Identifier name patterns — strict (exact or `_suffix`) to avoid false hits +// like "grid" / "valid" / "android". `rank` is intentionally excluded so a +// Rank field classifies as ordinal (a usable axis) rather than an identifier. +const ID_NAME_PATTERNS = ['id', 'index', 'idx', 'row', 'order', 'position', 'pos']; + +function looksLikeIdentifier(name: string): boolean { + const lower = name.toLowerCase(); + return ID_NAME_PATTERNS.some(p => lower === p || lower.endsWith('_' + p)); +} + +/** + * Classify one field into a single {@link ChartFieldRole}. The order of checks + * is significant: the most *specific* interpretation wins (geo coordinate → + * geo place → temporal → identifier → measure → ordinal → categorical). + */ +function classifyRole(name: string, type: string, semanticType: string): ChartFieldRole { + const st = semanticType; + // Geographic coordinates (Latitude / Longitude): numeric but not measures. + if (isGeoCoordinateType(st)) { + if (st === 'Latitude' || nameMatches(name, ['latitude', 'lat'])) return 'latitude'; + if (st === 'Longitude' || nameMatches(name, ['longitude', 'lon', 'lng', 'long'])) return 'longitude'; + return 'other'; + } + // Named geographic places (Country / State / City / Region / …). + if (isGeoLocationString(st)) return 'geoPlace'; + // Time axis. + if (type === 'temporal' || isTimeSeriesType(st)) return 'temporal'; + // Row identifiers — demoted so they never become a category or measure. + if (st === 'ID' || looksLikeIdentifier(name)) return 'identifier'; + // True quantitative measure (mirrors isQuantitativeField in the encoders). + if (type === 'quantitative' && !isNonMeasureNumeric(st) && (isMeasureType(st) || st === '')) { + return 'measure'; + } + // Ordered discrete (Rank, Range, Score-as-ordinal, Direction, …). + if (isOrdinalType(st)) return 'ordinal'; + // Plain category. + if (type === 'nominal' || isCategoricalType(st)) return 'categorical'; + return 'other'; +} + +/** + * Build a {@link DataProfile} from raw rows + semantic-type annotations. + * Deterministic — no random sampling; classification is per-field and + * order-independent. + * + * @param data Array of row objects. + * @param semanticTypes Field → semantic-type map (e.g. `{ Entity: "Country" }`). + */ +export function profileData(data: any[], semanticTypes: Record): DataProfile { + const tv: InternalTableView = buildTableView(data, semanticTypes); + const fields: ProfiledField[] = tv.names.map(name => ({ + name, + type: tv.fieldType[name] ?? 'nominal', + semanticType: tv.fieldSemanticType[name] ?? '', + cardinality: tv.fieldLevels[name]?.length ?? 0, + role: classifyRole(name, tv.fieldType[name] ?? 'nominal', tv.fieldSemanticType[name] ?? ''), + })); + + const by = (r: ChartFieldRole) => fields.filter(f => f.role === r); + const categoricals = by('categorical'); + const ordinals = by('ordinal'); + const geoPlaces = by('geoPlace'); + const temporals = by('temporal'); + + return { + fields, + measures: by('measure'), + temporals, + categoricals, + ordinals, + geoPlaces, + latitudes: by('latitude'), + longitudes: by('longitude'), + identifiers: by('identifier'), + dimensions: [...categoricals, ...ordinals, ...geoPlaces, ...temporals], + rowCount: tv.rows.length, + }; +} + +// ── Scoring ───────────────────────────────────────────────────────────── + +/** A ranked chart-type candidate with its score and human-readable reasons. */ +export interface ChartTypeSuggestion { + chartType: string; + /** Higher is a better fit. Scores are relative; only the ordering is meaningful. */ + score: number; + reasons: string[]; +} + +/** Options for {@link recommendChartTypes} / {@link recommendChartTypesDetailed}. */ +export interface RecommendChartTypesOptions { + /** + * Restrict results to these chart-type names (e.g. a backend's template + * catalog). Types not in the list are dropped. Omit for all shared types. + */ + supportedTypes?: string[]; + /** Cap the number of suggestions returned (after ranking). */ + max?: number; +} + +/** + * A recommended chart type paired with its recommended channel → field + * encodings. Returned by the backend one-step recommenders + * (`vlRecommendCharts`, …) that chain type selection with encoding selection. + */ +export interface RecommendedChart { + chartType: string; + encodings: Record; +} + +// Cardinality thresholds for readability gates. +const LOW_CARD_SERIES = 12; // a legend/series stays readable up to ~12 colors +const LOW_CARD_AXIS = 25; // a discrete heatmap axis stays readable up to ~25 bands +const PIE_MAX_SLICES = 8; // a pie is legible only with a handful of slices + +/** Semantic types the choropleth base maps can actually render (see map.ts). */ +const CHOROPLETH_SEMANTIC = new Set(['Country', 'State']); +const CHOROPLETH_NAME_HINTS = ['country', 'state', 'province', 'nation']; + +/** + * Score candidate chart types for a data profile. Returns suggestions sorted by + * descending score; ties keep a stable, priority-ordered arrangement. + * + * The rules are curated design heuristics: more *specific* chart types + * (maps, time series) outrank generic ones (bar) when the data supports them, + * so a table with a geographic place + a measure surfaces "Choropleth" ahead of + * "Bar Chart" without a model call. + */ +export function rankChartTypes(profile: DataProfile): ChartTypeSuggestion[] { + const { + measures, temporals, categoricals, ordinals, geoPlaces, + latitudes, longitudes, rowCount, + } = profile; + + const catLike = [...categoricals, ...ordinals, ...geoPlaces]; // discrete, non-temporal + const dims = profile.dimensions; + const lowCardCatLike = catLike.filter(f => f.cardinality >= 2 && f.cardinality <= LOW_CARD_SERIES); + const lowCardDims = dims.filter(f => f.cardinality >= 2 && f.cardinality <= LOW_CARD_AXIS); + const hasMeasure = measures.length >= 1; + + // Preserve insertion order for stable tie-breaking; keep the max score per type. + const order: string[] = []; + const acc = new Map(); + const add = (chartType: string, score: number, reason: string) => { + const cur = acc.get(chartType); + if (!cur) { + order.push(chartType); + acc.set(chartType, { score, reasons: [reason] }); + } else { + if (score > cur.score) cur.score = score; + if (!cur.reasons.includes(reason)) cur.reasons.push(reason); + } + }; + + // ── Geographic (most specific) ────────────────────────────────────── + if (latitudes.length >= 1 && longitudes.length >= 1) { + add('Map', 96, 'has latitude + longitude coordinates'); + } + const choroGeo = geoPlaces.filter( + f => CHOROPLETH_SEMANTIC.has(f.semanticType) || nameMatches(f.name, CHOROPLETH_NAME_HINTS), + ); + if (choroGeo.length >= 1 && hasMeasure) { + add('Choropleth', 92, 'has a geographic region field + a measure'); + } + + // ── Time series ───────────────────────────────────────────────────── + if (temporals.length >= 1 && hasMeasure) { + add('Line Chart', 88, 'has a time field + a measure (trend over time)'); + add('Area Chart', 66, 'has a time field + a measure'); + } + + // ── Correlation (two measures) ────────────────────────────────────── + if (measures.length >= 2) { + add('Scatter Plot', 84, 'has two or more measures (relationship)'); + } + + // ── Category + measure (bar) ──────────────────────────────────────── + if (dims.length >= 1 && hasMeasure) { + add('Bar Chart', 80, 'has a category axis + a measure'); + } + + // ── Single-measure distribution (histogram) ───────────────────────── + if (hasMeasure && dims.length === 0) { + add('Histogram', 82, 'has a measure with no category (distribution)'); + } + + // ── Two-category comparison (grouped / stacked / heatmap) ─────────── + if (catLike.length >= 2 && lowCardCatLike.length >= 1 && hasMeasure) { + add('Grouped Bar Chart', 72, 'has two categories + a measure'); + add('Stacked Bar Chart', 70, 'has two categories + a measure'); + } + if (lowCardDims.length >= 2 && hasMeasure) { + add('Heatmap', 74, 'has two discrete fields + a measure (matrix)'); + } + + // ── Part-to-whole (pie) ───────────────────────────────────────────── + if (catLike.length === 1 && temporals.length === 0 && hasMeasure) { + const c = catLike[0]; + const oneRowPerCategory = rowCount <= c.cardinality * 1.5; + if (c.cardinality >= 2 && c.cardinality <= PIE_MAX_SLICES && oneRowPerCategory) { + add('Pie Chart', 64, 'a few categories that sum to a whole'); + } + } + + // ── Distribution across groups (box / strip) ──────────────────────── + if (catLike.length >= 1 && hasMeasure && temporals.length === 0) { + const smallest = catLike.reduce((a, b) => (b.cardinality < a.cardinality ? b : a)); + if (smallest.cardinality >= 1 && rowCount >= smallest.cardinality * 2) { + add('Boxplot', 58, 'multiple measure values per group (spread)'); + add('Strip Plot', 52, 'multiple measure values per group'); + } + } + + // ── Count-only fallbacks (dimensions but no measure) ──────────────── + if (!hasMeasure && dims.length >= 1) { + add('Bar Chart', 60, 'categories without a measure (counts)'); + if (lowCardDims.length >= 2) add('Heatmap', 55, 'two discrete fields (cross-tab counts)'); + } + + // ── Last-resort fallback ──────────────────────────────────────────── + if (order.length === 0) { + if (measures.length >= 2) add('Scatter Plot', 20, 'fallback for numeric data'); + else add('Bar Chart', 15, 'fallback'); + } + + return order + .map(chartType => ({ chartType, score: acc.get(chartType)!.score, reasons: acc.get(chartType)!.reasons })) + // Stable sort (ES2019+) — equal scores keep priority insertion order. + .sort((a, b) => b.score - a.score); +} + +// ── Public API ────────────────────────────────────────────────────────── + +/** + * Recommend candidate chart types for a dataset, with scores and reasons. + * + * @param data Array of row objects. + * @param semanticTypes Field → semantic-type map (e.g. `{ Entity: "Country" }`). + * @param options Optional `supportedTypes` filter and `max` cap. + * @returns Ranked {@link ChartTypeSuggestion}s (best first). + */ +export function recommendChartTypesDetailed( + data: any[], + semanticTypes: Record, + options: RecommendChartTypesOptions = {}, +): ChartTypeSuggestion[] { + const profile = profileData(data, semanticTypes); + let ranked = rankChartTypes(profile); + if (options.supportedTypes) { + const allowed = new Set(options.supportedTypes); + ranked = ranked.filter(s => allowed.has(s.chartType)); + } + if (options.max != null) ranked = ranked.slice(0, options.max); + return ranked; +} + +/** + * Recommend a ranked list of chart-type names for a dataset (best first). + * + * A convenience wrapper over {@link recommendChartTypesDetailed} that drops the + * scores/reasons. This is the "type-selection step that sits in front of" + * `recommendChannels` — call it first, then feed the chosen type to + * `…RecommendEncodings` to populate channels. + * + * @param data Array of row objects. + * @param semanticTypes Field → semantic-type map. + * @param options Optional `supportedTypes` filter and `max` cap. + * @returns Ranked chart-type names (best first). + */ +export function recommendChartTypes( + data: any[], + semanticTypes: Record, + options: RecommendChartTypesOptions = {}, +): string[] { + return recommendChartTypesDetailed(data, semanticTypes, options).map(s => s.chartType); +} diff --git a/packages/flint-js/src/core/index.ts b/packages/flint-js/src/core/index.ts index e9ddd53f..9b979e40 100644 --- a/packages/flint-js/src/core/index.ts +++ b/packages/flint-js/src/core/index.ts @@ -139,6 +139,20 @@ export { type SemanticRole, } from './recommendation'; +// Data-driven chart *type* recommendation +export { + profileData, + rankChartTypes, + recommendChartTypes, + recommendChartTypesDetailed, + type ChartFieldRole, + type ProfiledField, + type DataProfile, + type ChartTypeSuggestion, + type RecommendChartTypesOptions, + type RecommendedChart, +} from './chart-type-recommendation'; + // Field semantics export { type SemanticAnnotation, diff --git a/packages/flint-js/src/core/recommendation.ts b/packages/flint-js/src/core/recommendation.ts index 879ba927..e5ef8890 100644 --- a/packages/flint-js/src/core/recommendation.ts +++ b/packages/flint-js/src/core/recommendation.ts @@ -710,16 +710,18 @@ export function pick( } } if (candidates.length === 0) return undefined; - // Prefer fields from the user's existing encodings when available + // Deterministic selection: prefer a field the user already uses, otherwise + // the first candidate in table order. Stable, so a given dataset always + // yields the same suggestion (no random tie-break). if (tv.preferredFields) { const preferred = candidates.filter(n => tv.preferredFields!.has(n)); if (preferred.length > 0) { - const chosen = preferred[Math.floor(Math.random() * preferred.length)]; + const chosen = preferred[0]; used.add(chosen); return chosen; } } - const chosen = candidates[Math.floor(Math.random() * candidates.length)]; + const chosen = candidates[0]; used.add(chosen); return chosen; } @@ -797,6 +799,22 @@ function isValidGroupingField(tv: InternalTableView, xField: string, colorField: return true; } +/** + * Deterministic grouping/series selection: choose the lowest-cardinality + * candidate (fewest distinct values → the most readable legend), breaking ties + * by table order. Replaces an earlier random tie-break so recommendations are + * stable and prefer compact color/series encodings. + */ +function lowestCardinality(tv: InternalTableView, candidates: string[]): string { + let best = candidates[0]; + let bestCard = tv.fieldLevels[best]?.length ?? Infinity; + for (let i = 1; i < candidates.length; i++) { + const card = tv.fieldLevels[candidates[i]]?.length ?? Infinity; + if (card < bestCard) { best = candidates[i]; bestCard = card; } + } + return best; +} + export function pickValidGroupingField( tv: InternalTableView, used: Set, xField: string, maxCard = 20, ): string | undefined { @@ -812,16 +830,17 @@ export function pickValidGroupingField( if (isValidGroupingField(tv, xField, name)) candidates.push(name); } if (candidates.length === 0) return undefined; - // Prefer fields from existing encodings + // Prefer a field the user already uses; otherwise the lowest-cardinality + // valid grouping field (fewest colors). Deterministic — no random pick. if (tv.preferredFields) { const preferred = candidates.filter(n => tv.preferredFields!.has(n)); if (preferred.length > 0) { - const chosen = preferred[Math.floor(Math.random() * preferred.length)]; + const chosen = lowestCardinality(tv, preferred); used.add(chosen); return chosen; } } - const chosen = candidates[Math.floor(Math.random() * candidates.length)]; + const chosen = lowestCardinality(tv, candidates); used.add(chosen); return chosen; } @@ -864,16 +883,17 @@ export function pickLineChartColorField( if (isValidLineSeriesData(tv, xField, name)) candidates.push(name); } if (candidates.length === 0) return undefined; - // Prefer fields from existing encodings + // Prefer a field the user already uses; otherwise the lowest-cardinality + // valid series field (fewest lines/colors). Deterministic — no random pick. if (tv.preferredFields) { const preferred = candidates.filter(n => tv.preferredFields!.has(n)); if (preferred.length > 0) { - const chosen = preferred[Math.floor(Math.random() * preferred.length)]; + const chosen = lowestCardinality(tv, preferred); used.add(chosen); return chosen; } } - const chosen = candidates[Math.floor(Math.random() * candidates.length)]; + const chosen = lowestCardinality(tv, candidates); used.add(chosen); return chosen; } @@ -989,11 +1009,14 @@ export function getRecommendation(chartType: string, tv: InternalTableView): Rec const xField = pickDiscrete(tv, used); const yField = pickQuantitative(tv, used); if (!xField || !yField) return {}; - const colorField = pickValidGroupingField(tv, used, xField, 20); - if (!colorField) return {}; + const seriesField = pickValidGroupingField(tv, used, xField, 20); + if (!seriesField) return {}; assign('x', xField); assign('y', yField); - assign('color', colorField); + // A grouped bar dodges by the `group` channel (not `color`) across + // all three backends — assigning `color` here would be dropped by + // the Vega-Lite template, leaving an ungrouped bar. + assign('group', seriesField); break; } diff --git a/packages/flint-js/src/echarts/index.ts b/packages/flint-js/src/echarts/index.ts index 5619a199..501abae9 100644 --- a/packages/flint-js/src/echarts/index.ts +++ b/packages/flint-js/src/echarts/index.ts @@ -31,4 +31,4 @@ export { } from './templates'; // EC recommendation & adaptation -export { ecAdaptChart, ecRecommendEncodings } from './recommendation'; +export { ecAdaptChart, ecRecommendEncodings, ecRecommendChartTypes, ecRecommendCharts } from './recommendation'; diff --git a/packages/flint-js/src/echarts/recommendation.ts b/packages/flint-js/src/echarts/recommendation.ts index 2b866186..af28884a 100644 --- a/packages/flint-js/src/echarts/recommendation.ts +++ b/packages/flint-js/src/echarts/recommendation.ts @@ -17,7 +17,12 @@ import { pickDiscrete, pickLowCardDiscrete, } from '../core/recommendation'; -import { ecGetTemplateChannels } from './templates'; +import { + recommendChartTypes, + type RecommendChartTypesOptions, + type RecommendedChart, +} from '../core/chart-type-recommendation'; +import { ecGetTemplateChannels, ecAllTemplateDefs } from './templates'; // ── EC-extended recommendation ────────────────────────────────────────── @@ -103,3 +108,36 @@ export function ecRecommendEncodings( } return result; } + +/** Chart-type names ECharts can render (its template catalog). */ +const EC_SUPPORTED_TYPES = ecAllTemplateDefs.map(d => d.chart); + +/** + * Recommend a ranked list of ECharts chart types for a dataset, restricted to + * chart types ECharts can render. See {@link ecRecommendCharts} for the + * one-step variant that also fills channels. + */ +export function ecRecommendChartTypes( + data: any[], + semanticTypes: Record, + options: Omit = {}, +): string[] { + return recommendChartTypes(data, semanticTypes, { ...options, supportedTypes: EC_SUPPORTED_TYPES }); +} + +/** + * One-step recommendation: rank ECharts chart types for the data, then populate + * each with {@link ecRecommendEncodings}. Suggestions whose required channels + * cannot be filled are dropped, so every returned chart is renderable. + */ +export function ecRecommendCharts( + data: any[], + semanticTypes: Record, + options: Omit = {}, +): RecommendedChart[] { + const { max, ...rest } = options; + const charts = ecRecommendChartTypes(data, semanticTypes, rest) + .map(chartType => ({ chartType, encodings: ecRecommendEncodings(chartType, data, semanticTypes) })) + .filter(s => Object.keys(s.encodings).length > 0); + return max != null ? charts.slice(0, max) : charts; +} diff --git a/packages/flint-js/src/vegalite/index.ts b/packages/flint-js/src/vegalite/index.ts index 35234919..f23c5510 100644 --- a/packages/flint-js/src/vegalite/index.ts +++ b/packages/flint-js/src/vegalite/index.ts @@ -25,4 +25,4 @@ export { } from './templates'; // VL recommendation & adaptation -export { vlAdaptChart, vlRecommendEncodings } from './recommendation'; +export { vlAdaptChart, vlRecommendEncodings, vlRecommendChartTypes, vlRecommendCharts } from './recommendation'; diff --git a/packages/flint-js/src/vegalite/recommendation.ts b/packages/flint-js/src/vegalite/recommendation.ts index 690b3ec5..52ea6c49 100644 --- a/packages/flint-js/src/vegalite/recommendation.ts +++ b/packages/flint-js/src/vegalite/recommendation.ts @@ -29,7 +29,12 @@ import { isValidLineSeriesData, nameMatches, } from '../core/recommendation'; -import { vlGetTemplateChannels } from './templates'; +import { + recommendChartTypes, + type RecommendChartTypesOptions, + type RecommendedChart, +} from '../core/chart-type-recommendation'; +import { vlGetTemplateChannels, vlAllTemplateDefs } from './templates'; // ── VL-extended recommendation ────────────────────────────────────────── @@ -237,3 +242,49 @@ export function vlRecommendEncodings( } return result; } + +/** Chart-type names Vega-Lite can render (its template catalog). */ +const VL_SUPPORTED_TYPES = vlAllTemplateDefs.map(d => d.chart); + +/** + * Recommend a ranked list of Vega-Lite chart types for a dataset. + * + * Wraps the backend-agnostic `recommendChartTypes`, restricting results to + * chart types Vega-Lite can render. Call it first, then feed the chosen type to + * {@link vlRecommendEncodings} to populate channels — or use + * {@link vlRecommendCharts} to do both in one step. + * + * @param data Array of row objects. + * @param semanticTypes Field→semantic-type map (e.g. `{ Entity: "Country" }`). + * @param options Optional `max` cap on the number of suggestions. + * @returns Ranked VL chart-type names (best first). + */ +export function vlRecommendChartTypes( + data: any[], + semanticTypes: Record, + options: Omit = {}, +): string[] { + return recommendChartTypes(data, semanticTypes, { ...options, supportedTypes: VL_SUPPORTED_TYPES }); +} + +/** + * One-step recommendation: rank Vega-Lite chart types for the data, then + * populate each with {@link vlRecommendEncodings}. Suggestions whose required + * channels cannot be filled are dropped, so every returned chart is renderable. + * + * @param data Array of row objects. + * @param semanticTypes Field→semantic-type map. + * @param options Optional `max` cap on the number of charts returned. + * @returns Ranked `{ chartType, encodings }` pairs (best first). + */ +export function vlRecommendCharts( + data: any[], + semanticTypes: Record, + options: Omit = {}, +): RecommendedChart[] { + const { max, ...rest } = options; + const charts = vlRecommendChartTypes(data, semanticTypes, rest) + .map(chartType => ({ chartType, encodings: vlRecommendEncodings(chartType, data, semanticTypes) })) + .filter(s => Object.keys(s.encodings).length > 0); + return max != null ? charts.slice(0, max) : charts; +} diff --git a/packages/flint-js/tests/chart-type-recommendation.test.ts b/packages/flint-js/tests/chart-type-recommendation.test.ts new file mode 100644 index 00000000..25ccc80f --- /dev/null +++ b/packages/flint-js/tests/chart-type-recommendation.test.ts @@ -0,0 +1,211 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { describe, it, expect } from 'vitest'; +import { + profileData, + recommendChartTypes, + recommendChartTypesDetailed, + vlRecommendChartTypes, + vlRecommendCharts, + vlRecommendEncodings, + ecRecommendChartTypes, + cjsRecommendChartTypes, +} from '../src'; + +// ── The "Life Expectancy Gap" table from the handoff ────────────────────── +// Entity/Code are Country (geo place), Year is temporal, the diff is a measure. +const LIFE_EXP = (() => { + const entities = [ + ['Afghanistan', 'AFG'], + ['Albania', 'ALB'], + ['Algeria', 'DZA'], + ]; + const years = [1950, 1951, 1952]; + const rows: any[] = []; + for (const [Entity, Code] of entities) { + for (const Year of years) { + rows.push({ Entity, Code, Year, LifeExpectancyDiffFM: 1.2 + rows.length * 0.03 }); + } + } + return rows; +})(); +const LIFE_EXP_TYPES = { + Entity: 'Country', + Code: 'Country', + Year: 'Year', + LifeExpectancyDiffFM: 'Number', +}; + +describe('data profiling', () => { + it('classifies fields into roles from semantic types', () => { + const p = profileData(LIFE_EXP, LIFE_EXP_TYPES); + expect(p.geoPlaces.map(f => f.name).sort()).toEqual(['Code', 'Entity']); + expect(p.temporals.map(f => f.name)).toEqual(['Year']); + expect(p.measures.map(f => f.name)).toEqual(['LifeExpectancyDiffFM']); + expect(p.categoricals).toHaveLength(0); + // geo places + the time field are all category-axis-capable dimensions. + expect(p.dimensions.map(f => f.name).sort()).toEqual(['Code', 'Entity', 'Year']); + expect(p.rowCount).toBe(9); + }); + + it('classifies latitude / longitude as coordinates, not measures', () => { + const p = profileData( + [{ lat: 40.7, lon: -74, mag: 5.1 }], + { lat: 'Latitude', lon: 'Longitude', mag: 'Number' }, + ); + expect(p.latitudes.map(f => f.name)).toEqual(['lat']); + expect(p.longitudes.map(f => f.name)).toEqual(['lon']); + expect(p.measures.map(f => f.name)).toEqual(['mag']); + }); + + it('demotes id-like fields so they are not measures or categories', () => { + const p = profileData( + [{ user_id: 1, amount: 10 }], + { user_id: 'ID', amount: 'Amount' }, + ); + expect(p.identifiers.map(f => f.name)).toEqual(['user_id']); + expect(p.measures.map(f => f.name)).toEqual(['amount']); + }); +}); + +describe('recommendChartTypes — the headline geographic case', () => { + it('ranks Choropleth first, then Line and Bar for country + time + measure', () => { + const types = recommendChartTypes(LIFE_EXP, LIFE_EXP_TYPES); + expect(types.slice(0, 3)).toEqual(['Choropleth', 'Line Chart', 'Bar Chart']); + }); + + it('exposes scores and reasons in the detailed form', () => { + const detailed = recommendChartTypesDetailed(LIFE_EXP, LIFE_EXP_TYPES); + expect(detailed[0].chartType).toBe('Choropleth'); + expect(detailed[0].score).toBeGreaterThan(detailed[1].score - 1); // sorted desc + expect(detailed[0].reasons.join(' ')).toMatch(/geographic/i); + // Monotonically non-increasing scores. + for (let i = 1; i < detailed.length; i++) { + expect(detailed[i - 1].score).toBeGreaterThanOrEqual(detailed[i].score); + } + }); + + it('is deterministic across repeated calls', () => { + const a = recommendChartTypes(LIFE_EXP, LIFE_EXP_TYPES); + const b = recommendChartTypes(LIFE_EXP, LIFE_EXP_TYPES); + expect(a).toEqual(b); + }); +}); + +describe('recommendChartTypes — other data shapes', () => { + const first = (data: any[], types: Record) => + recommendChartTypes(data, types)[0]; + + it('picks Line Chart for a time series', () => { + expect(first( + [{ date: '2020-01-01', sales: 3 }, { date: '2020-02-01', sales: 5 }], + { date: 'Date', sales: 'Amount' }, + )).toBe('Line Chart'); + }); + + it('picks Bar Chart for a single category + measure', () => { + const types = recommendChartTypes( + [{ region: 'A', sales: 3 }, { region: 'B', sales: 5 }, { region: 'C', sales: 2 }, { region: 'D', sales: 4 }], + { region: 'Category', sales: 'Amount' }, + ); + expect(types[0]).toBe('Bar Chart'); + expect(types).toContain('Pie Chart'); // few categories, one row each → part-to-whole + }); + + it('picks Scatter Plot for two measures', () => { + expect(first( + [{ height: 1, weight: 2 }, { height: 3, weight: 4 }], + { height: 'Number', weight: 'Number' }, + )).toBe('Scatter Plot'); + }); + + it('picks Histogram for a lone measure', () => { + expect(first([{ v: 1 }, { v: 2 }, { v: 3 }], { v: 'Number' })).toBe('Histogram'); + }); + + it('picks Map for latitude + longitude', () => { + expect(first( + [{ lat: 40.7, lon: -74, mag: 5 }, { lat: 34, lon: -118, mag: 4 }], + { lat: 'Latitude', lon: 'Longitude', mag: 'Number' }, + )).toBe('Map'); + }); +}); + +describe('recommendChartTypes — options', () => { + it('filters to supportedTypes, preserving rank order', () => { + const types = recommendChartTypes(LIFE_EXP, LIFE_EXP_TYPES, { + supportedTypes: ['Bar Chart', 'Line Chart'], + }); + expect(types).toEqual(['Line Chart', 'Bar Chart']); + }); + + it('caps the number of suggestions with max', () => { + expect(recommendChartTypes(LIFE_EXP, LIFE_EXP_TYPES, { max: 2 })) + .toEqual(['Choropleth', 'Line Chart']); + }); +}); + +describe('backend wrappers', () => { + it('vlRecommendChartTypes restricts to Vega-Lite types and keeps Choropleth first', () => { + const types = vlRecommendChartTypes(LIFE_EXP, LIFE_EXP_TYPES); + expect(types[0]).toBe('Choropleth'); + }); + + it('ecRecommendChartTypes / cjsRecommendChartTypes only return their own catalog', () => { + // ECharts and Chart.js have no Choropleth/Map template, so those geographic + // types must be filtered out even though the data is geographic. + const ec = ecRecommendChartTypes(LIFE_EXP, LIFE_EXP_TYPES); + const cjs = cjsRecommendChartTypes(LIFE_EXP, LIFE_EXP_TYPES); + expect(ec).not.toContain('Choropleth'); + expect(ec).not.toContain('Map'); + expect(ec.length).toBeGreaterThan(0); + expect(cjs).not.toContain('Choropleth'); + expect(cjs.length).toBeGreaterThan(0); + }); + + it('vlRecommendCharts pairs each type with fillable encodings (one step)', () => { + const charts = vlRecommendCharts(LIFE_EXP, LIFE_EXP_TYPES); + // Every returned chart is renderable (non-empty encodings). + for (const c of charts) { + expect(Object.keys(c.encodings).length).toBeGreaterThan(0); + } + // The top pick is a Choropleth with the region on `id` and the measure on `color`. + const choropleth = charts.find(c => c.chartType === 'Choropleth')!; + expect(choropleth).toBeDefined(); + expect(['Entity', 'Code']).toContain(choropleth.encodings.id); + expect(choropleth.encodings.color).toBe('LifeExpectancyDiffFM'); + }); +}); + +describe('encoding recommendation is deterministic and quality-aware', () => { + it('vlRecommendEncodings returns the same result on repeated calls', () => { + const runs = Array.from({ length: 5 }, () => + vlRecommendEncodings('Line Chart', LIFE_EXP, LIFE_EXP_TYPES), + ); + for (const r of runs) expect(r).toEqual(runs[0]); + // Time on x, measure on y, a country as the series. + expect(runs[0].x).toBe('Year'); + expect(runs[0].y).toBe('LifeExpectancyDiffFM'); + expect(['Entity', 'Code']).toContain(runs[0].color); + }); + + it('prefers the lowest-cardinality field for the color/series channel', () => { + // `label` (12 unique) makes every (label, series) pair unique, so both + // `tier` (2) and `group` (4) are valid grouping fields. The recommender + // must choose the smaller one for a more readable legend. + const rows = Array.from({ length: 12 }, (_, i) => ({ + label: `L${i}`, + tier: i % 2 === 0 ? 'lo' : 'hi', + grp: `g${i % 4}`, + val: i, + })); + const enc = vlRecommendEncodings('Grouped Bar Chart', rows, { + label: 'Category', tier: 'Category', grp: 'Category', val: 'Amount', + }); + expect(enc.x).toBe('label'); + expect(enc.y).toBe('val'); + // Grouped bars dodge by the `group` channel; the series is the 2-value field. + expect(enc.group).toBe('tier'); + }); +}); From be21aa8e239b142a39bc45fa7607be04e46b1f7e Mon Sep 17 00:00:00 2001 From: Anon-link <1149559789@qq.com> Date: Fri, 17 Jul 2026 16:06:25 +0800 Subject: [PATCH 2/9] add flint-chinese --- docs/zh-CN/DEVELOPMENT.md | 72 ++ docs/zh-CN/adding-a-backend.md | 138 +++ docs/zh-CN/adding-a-chart-template.md | 133 +++ docs/zh-CN/adding-a-semantic-type.md | 128 +++ docs/zh-CN/api-reference.md | 265 +++++ docs/zh-CN/architecture.md | 195 ++++ docs/zh-CN/color-decisions.md | 207 ++++ docs/zh-CN/design-semantics.md | 863 ++++++++++++++++ docs/zh-CN/design-stretch-model.md | 1096 +++++++++++++++++++++ docs/zh-CN/overview.md | 182 ++++ docs/zh-CN/reference-chartjs.md | 194 ++++ docs/zh-CN/reference-echarts.md | 336 +++++++ docs/zh-CN/reference-vegalite.md | 429 ++++++++ docs/zh-CN/tutorials/agent-workflows.md | 326 ++++++ docs/zh-CN/tutorials/chart-sizing.md | 105 ++ docs/zh-CN/tutorials/data-story.md | 199 ++++ docs/zh-CN/tutorials/getting-started.md | 150 +++ docs/zh-CN/tutorials/setup-flint-mcp.md | 198 ++++ package-lock.json | 97 +- site/package.json | 2 + site/src/components/ChartCodeModal.tsx | 20 +- site/src/components/GalleryOptionsBar.tsx | 6 +- site/src/components/MarkdownView.tsx | 11 +- site/src/components/SiteShell.tsx | 123 ++- site/src/i18n/LocaleContext.tsx | 76 ++ site/src/i18n/LocaleLink.tsx | 12 + site/src/i18n/index.ts | 18 + site/src/i18n/locales.ts | 21 + site/src/i18n/messages/en.json | 283 ++++++ site/src/i18n/messages/zh-CN.json | 283 ++++++ site/src/i18n/paths.ts | 53 + site/src/main.tsx | 59 +- site/src/routes/ChartWall.tsx | 67 +- site/src/routes/DocSectionPage.tsx | 55 +- site/src/routes/Editor.tsx | 19 +- site/src/routes/Landing.tsx | 338 ++++--- site/src/routes/McpServer.tsx | 162 ++- site/src/shared/editor-payload.ts | 20 +- site/src/shared/load-docs.ts | 48 +- 39 files changed, 6607 insertions(+), 382 deletions(-) create mode 100644 docs/zh-CN/DEVELOPMENT.md create mode 100644 docs/zh-CN/adding-a-backend.md create mode 100644 docs/zh-CN/adding-a-chart-template.md create mode 100644 docs/zh-CN/adding-a-semantic-type.md create mode 100644 docs/zh-CN/api-reference.md create mode 100644 docs/zh-CN/architecture.md create mode 100644 docs/zh-CN/color-decisions.md create mode 100644 docs/zh-CN/design-semantics.md create mode 100644 docs/zh-CN/design-stretch-model.md create mode 100644 docs/zh-CN/overview.md create mode 100644 docs/zh-CN/reference-chartjs.md create mode 100644 docs/zh-CN/reference-echarts.md create mode 100644 docs/zh-CN/reference-vegalite.md create mode 100644 docs/zh-CN/tutorials/agent-workflows.md create mode 100644 docs/zh-CN/tutorials/chart-sizing.md create mode 100644 docs/zh-CN/tutorials/data-story.md create mode 100644 docs/zh-CN/tutorials/getting-started.md create mode 100644 docs/zh-CN/tutorials/setup-flint-mcp.md create mode 100644 site/src/i18n/LocaleContext.tsx create mode 100644 site/src/i18n/LocaleLink.tsx create mode 100644 site/src/i18n/index.ts create mode 100644 site/src/i18n/locales.ts create mode 100644 site/src/i18n/messages/en.json create mode 100644 site/src/i18n/messages/zh-CN.json create mode 100644 site/src/i18n/paths.ts diff --git a/docs/zh-CN/DEVELOPMENT.md b/docs/zh-CN/DEVELOPMENT.md new file mode 100644 index 00000000..ccfe6881 --- /dev/null +++ b/docs/zh-CN/DEVELOPMENT.md @@ -0,0 +1,72 @@ +# 开发指南 + +使用本页在本地搭建 **flint-chart**、运行常用检查,并在需要添加新能力时找到合适的扩展路径。 + +## 前置条件 + +- Node 18+(见 [`packages/flint-js/.nvmrc`](../packages/flint-js/.nvmrc);若使用 nvm,请运行 `nvm use`) +- npm 9+(workspaces) + +## 首次设置 + +```bash +git clone https://github.com/microsoft/flint-chart +cd flint-chart +npm install # root workspace: packages/flint-js, packages/flint-mcp, site +``` + +## 日常命令 + +在**仓库根目录**运行以下命令: + +| 命令 | 作用 | +|---------|----------------| +| `npm run typecheck` | 构建/类型检查 `packages/flint-js`,并对 `packages/flint-mcp` 做类型检查 | +| `npm run test` | 在 `packages/flint-js` 和 `packages/flint-mcp` 中运行 Vitest | +| `npm run build` | 构建 `packages/flint-js` 和 `packages/flint-mcp` | +| `npm run site` | 演示站点,地址 http://localhost:5274/ | +| `npm run site:build` | 生产构建 → `site/dist/` | +| `npm run build:mcp` | 构建 MCP 服务器 workspace | + +演示站点通过 Vite 将 `flint-chart` 别名指向 `packages/flint-js/src`,因此库代码的修改会在画廊和编辑器中热重载,无需重新构建 `dist/`。 + +## 仓库结构 + +``` +flint-chart/ +├── packages/ +│ ├── flint-js/ npm package `flint-chart` +│ │ ├── src/core/ semantics, layout, types +│ │ ├── src/vegalite/ Vega-Lite backend +│ │ ├── src/echarts/ ECharts backend +│ │ ├── src/chartjs/ Chart.js backend +│ │ └── src/test-data/ gallery fixtures +│ ├── flint-py/ Python port preview (PyPI package planned later) +│ └── flint-mcp/ npm package `flint-chart-mcp` +├── site/ landing, gallery, editor, docs browser +├── docs/ architecture + site documentation sources +├── agent-skills/ AI agent skill (SKILL.md) +└── shared/test-data/ JSON fixtures (JS + Python) +``` + +## 图表组装流程 + +1. **Phase 0 — 语义解析**(`packages/flint-js/src/core/resolve-semantics.ts`) +2. **Phase 1 — 布局**(`packages/flint-js/src/core/compute-layout.ts`) +3. **Phase 2 — 实例化**(各后端的 `assemble.ts` + templates) + +完整流程见 [Architecture](/documentation/architecture)。 + +## 扩展指南 + +根据你要扩展的层面选择对应指南: + +- [Extending chart templates](/documentation/adding-a-chart-template) — 在现有后端中添加新图表类型。 +- [Extending semantic types](/documentation/adding-a-semantic-type) — 让 Flint 识别新的字段含义,从而改变格式化、聚合、比例尺或颜色行为。 +- [Extending backends](/documentation/adding-a-backend) — 添加消费共享编译器输出的新渲染目标。 + +## 测试覆盖 + +- **冒烟测试:** `packages/flint-js/tests/smoke.test.ts` +- **视觉覆盖:** [Gallery](/gallery),由 test-data 中的 `TEST_GENERATORS` 驱动 +- **共享 fixtures:** `shared/test-data/`,供 JS 与 Python 测试共用 diff --git a/docs/zh-CN/adding-a-backend.md b/docs/zh-CN/adding-a-backend.md new file mode 100644 index 00000000..d94f96c5 --- /dev/null +++ b/docs/zh-CN/adding-a-backend.md @@ -0,0 +1,138 @@ +# 扩展后端 + +当 Flint 需要面向新的渲染库或 spec 格式时,添加后端。后端是 `assemble(input)` 编排器加上 `templates/` 注册表;二者共同将共享编译器输出转换为原生图表 spec。现有参考实现位于 `packages/flint-js/src/` 下的 `vegalite/`、`echarts/` 和 `chartjs/`。 + +流水线阶段与仓库结构见 [Architecture](/documentation/architecture)。 + +--- + +## 目录 + +- [§1 创建骨架](#1-创建骨架) +- [§2 遵循组装契约](#2-遵循组装契约) +- [§3 添加模板](#3-添加模板) +- [§4 接入包](#4-接入包) +- [§5 站点与画廊](#5-站点与画廊) +- [§6 验收清单](#6-验收清单) +- [§7 相关文档](#7-相关文档) + +--- + +# §1 创建骨架 + +``` +packages/flint-js/src// +├── index.ts # public barrel +├── assemble.ts # orchestrator: ChartAssemblyInput → native spec +├── instantiate-spec.ts # encoding + layout → spec (optional; some backends inline this) +├── recommendation.ts # chart-type recommendations (optional) +└── templates/ + ├── index.ts # category map + getTemplateDef() + ├── bar.ts + ├── line.ts + └── … +``` + +从零开始前,先复制最接近的现有后端。Vega-Lite 是共享流水线最完整的参考;ECharts 额外包含 `colormap.ts` 和 `facet.ts` 以处理后端特定关注点。 + +--- + +# §2 遵循组装契约 + +```typescript +function assemble(input: ChartAssemblyInput): +``` + +`ChartAssemblyInput` 定义于 `packages/flint-js/src/core/types.ts`,包含 `data`、`chart_spec`、`semantic_types`、`options` 及相关字段。 + +### 流水线(不要跳过 core 阶段) + +编排器**协调** `core/`,不应从原始字段类型重新推导格式、零基线或颜色。 + +```text +PRE-PHASE normalizeStaticSeries(), applyEncodingOverrides() + (may need a preliminary resolveChannelSemantics for types) + +PHASE 0 resolveChannelSemantics() → Record + computeZeroDecision() per quantitative x/y (needs template mark) + chartProperties overrides (includeZero_*, logScale_*, …) + +STEP 0a template.declareLayoutMode?.() → LayoutDeclaration + +STEP 0b convertTemporalData() + +STEP 0c computeChannelBudgets() + filterOverflow() + +PHASE 1 computeLayout() → LayoutResult + +PHASE 2 build backend encodings + template.instantiate(spec, InstantiateContext) + apply layout (vlApplyLayoutToSpec / ecApplyLayoutToSpec / …) + postProcess?, tooltips, facet combine +``` + +规范顺序见 `packages/flint-js/src/vegalite/assemble.ts`(文件头 + `assembleVegaLite`)。 + +**IR 边界:** 下游代码读取扁平的 `ChannelSemantics` 和 `LayoutResult`,而不是重新检查语义类型字符串。 + +--- + +# §3 添加模板 + +模板编码**形状**,而非**决策**。若模板需要按 `field.type === 'temporal'` 分支,应将该逻辑移到 `core/`。 + +每个模板导出一个 `ChartTemplateDef`(`core/types.ts`): + +| 字段 | 作用 | +|---|---| +| `chart` | 显示名称 — 必须与 `chart_spec.chartType` 一致 | +| `template` | 原生 spec 骨架(mark + encoding 结构) | +| `channels` | 允许的编码槽位 | +| `markCognitiveChannel` | `position` / `length` / `area` / `color` — 驱动零基线与压缩 | +| `declareLayoutMode?` | 布局前的轴标志(banded vs continuous、σ 覆盖) | +| `instantiate` | 根据 `InstantiateContext` 修改 spec(encodings、layout、semantics) | +| `properties?` | 可配置的图表属性 | +| `postProcess?` | 布局后的最终视觉微调 | + +在 `templates/index.ts` 中注册:导入 defs,加入 category map,并暴露 `*GetTemplateDef(chartType)` 为 `find(t => t.chart === chartType)`。 + +--- + +# §4 接入包 + +1. **Barrel** — 在 `packages/flint-js/src/index.ts` 中 `export * from './'` +2. **Bundle** — 在 `packages/flint-js/tsup.config.ts` 中添加 `/index` 入口 +3. **Exports** — 在 `packages/flint-js/package.json#exports` 中添加 `"./"` 子路径 +4. **冒烟测试** — 在 `packages/flint-js/tests/smoke.test.ts` 中扩展一条 `assemble()` 形状断言 +5. **Gallery 数据** — 在 `src/test-data/` 中添加 `gen*Tests()`,并在 `TEST_GENERATORS` 中注册 + +--- + +# §5 站点与画廊 + +- **Gallery 开发服务器:** 在仓库根目录运行 `npm run site`,然后打开 `/gallery` +- **Supported backends:** 若新后端应出现在 UI 中,更新 `site/src/shared/supported-backends.ts` +- **Renderers:** 仅当 spec 格式无法复用 `VegaLiteView`、`EChartsView` 或 `ChartjsView` 时,才添加新的 React 视图(`site/src/components/`)。`TripleChart` 当前覆盖 VL + ECharts + Chart.js。 + +可选:若 MCP 客户端应能调用该组装器,将其接入 `agent-skills/mcp-server/`。 + +--- + +# §6 验收清单 + +后端就绪的标志: + +- [ ] Bar、line、area 和 scatter 模板在标准 gallery 矩阵上渲染正确 +- [ ] `tests/smoke.test.ts` 对新组装器通过 +- [ ] 仓库根目录的 `npm run typecheck` 和 `npm run test` 通过 +- [ ] 至少有一个专用 test-data 生成器覆盖后端特定选项 + +**对等说明:** 并非每个 `chart` 名称目前在各后端都存在。记录你移植了哪些模板;跨后端对等是目标,而非首次合并的前置条件。 + +--- + +# §7 相关文档 + +- [Extending chart templates](/documentation/adding-a-chart-template) — `ChartTemplateDef` 编写 +- [Auto Layout Algorithm](/documentation/layout-model) — `computeLayout()` 的期望输入 +- [API reference](/documentation/api-reference) — `ChartAssemblyInput` 与组装器入口 diff --git a/docs/zh-CN/adding-a-chart-template.md b/docs/zh-CN/adding-a-chart-template.md new file mode 100644 index 00000000..a2d35c10 --- /dev/null +++ b/docs/zh-CN/adding-a-chart-template.md @@ -0,0 +1,133 @@ +# 扩展图表模板 + +当后端已存在、你想为其添加另一种图表类型时使用本指南。若要添加新的渲染目标,请从 [Extending backends](/documentation/adding-a-backend) 开始。 + +--- + +## 目录 + +- [§1 选择图表名称与通道](#1-选择图表名称与通道) +- [§2 编写模板](#2-编写模板) +- [§3 注册模板](#3-注册模板) +- [§4 添加测试数据与 gallery 覆盖](#4-添加测试数据与-gallery-覆盖) +- [§5 跨后端对等](#5-跨后端对等) +- [§6 相关文档](#6-相关文档) + +--- + +# §1 选择图表名称与通道 + +公开标识是 `ChartTemplateDef` 上的 **`chart` 字符串**。它必须与 `chart_spec.chartType` 完全一致,例如 `"Scatter Plot"`。 + +只选择标记实际使用的通道。以相似模板为起点复制: + +| 族 | Vega-Lite 参考 | +|---|---| +| Scatter / point | `vegalite/templates/scatter.ts` | +| Bar / column | `vegalite/templates/bar.ts` | +| Line / area | `vegalite/templates/line.ts` | +| Radial | `vegalite/templates/pie.ts` | + +ECharts 与 Chart.js 在各自的 `templates/` 目录下使用相同的 `ChartTemplateDef` 接口。 + +--- + +# §2 编写模板 + +`ChartTemplateDef` 位于 `packages/flint-js/src/core/types.ts`。 + +```typescript +import { ChartTemplateDef } from '../../core/types'; +import { defaultBuildEncodings } from './utils'; + +export const dotPlotDef: ChartTemplateDef = { + chart: 'Dot Plot', + template: { mark: 'circle', encoding: {} }, + channels: ['x', 'y', 'color', 'size', 'column', 'row'], + markCognitiveChannel: 'position', + + declareLayoutMode: (channelSemantics, table, chartProperties) => { + // optional: banded axes, σ overrides, Q→O conversion + return { /* LayoutDeclaration */ }; + }, + + instantiate: (spec, ctx) => { + defaultBuildEncodings(spec, ctx.resolvedEncodings); + // ctx.channelSemantics, ctx.layout, ctx.table, ctx.chartProperties, … + }, + + properties: [ + { key: 'opacity', label: 'Opacity', type: 'continuous', + min: 0.1, max: 1, step: 0.05, defaultValue: 1 }, + ], +}; +``` + +### 关键规则 + +1. **`template`** — 最小原生骨架;`instantiate` 填充 encodings 与 mark 属性。 +2. **`markCognitiveChannel`** — 告诉编译器读者如何解码数值(影响零基线与 [Auto Layout Algorithm](/documentation/layout-model) 压缩)。 +3. **`instantiate`** — 接收 `template` 的**深拷贝**以及 `InstantiateContext`(已解析 encodings、`ChannelSemantics`、`LayoutResult`、数据表、画布尺寸)。 +4. **不做语义分支** — 读取 `ctx.channelSemantics[channel].format`、`.type`、`.zero` 等;不要按原始字段名或存储类型 switch。 + +可选钩子:`postProcess`(布局后)、`encodingActions`(shelf 快捷操作)。 + +--- + +# §3 注册模板 + +在 `packages/flint-js/src//templates/index.ts` 中: + +1. 导入新的 `*Def` 常量。 +2. 将其加入 `*TemplateDefs` 内合适的 category 数组(例如 `scatterTemplates`)。 +3. 确保 `*GetTemplateDef(chartType)` 能找到它:`defs.find(t => t.chart === chartType)`。 + +Vega-Lite 还会运行 `withInjectedProperties()`,为各模板附加共享的分面与对数比例尺属性。若你的图表需要相同钩子,请参照该文件中的现有条目。 + +--- + +# §4 添加测试数据与 gallery 覆盖 + +### 生成器模式 + +`TestCase` 接口位于 `packages/flint-js/src/test-data/types.ts`。 + +典型流程(见 `scatter-tests.ts`、`bar-tests.ts`): + +1. 定义小型参数矩阵(基数、是否着色、是否分面)。 +2. 导出 `genTests(): TestCase[]`,其中 `chartType` 与 `ChartTemplateDef.chart` 一致。 +3. 在 `packages/flint-js/src/test-data/index.ts` 中注册: + +```typescript +TEST_GENERATORS['Dot Plot'] = genDotPlotTests; +``` + +4. 可选地在 `gallery-tree.ts` 中添加列出该生成器键的页面。 + +### 验证 + +```bash +npm run typecheck +npm run test +npm run site # Gallery → find your chart type +``` + +在 3–6 个代表性用例上检查格式化、布局拉伸、图例与分面行为。 + +--- + +# §5 跨后端对等 + +面向用户的契约是:在模板存在的前提下,相同的 `chartType` 字符串应能在 `assembleVegaLite`、`assembleECharts` 和 `assembleChartjs` 上工作。实践中: + +- 先移植到你立即需要的后端;其余可另开跟进。 +- `site/src/shared/supported-backends.ts` 按各后端注册表过滤图表类型。仅 Vega-Lite 的模板在注册到 ECharts 之前不会出现在 ECharts 中。 + +--- + +# §6 相关文档 + +- [Extending backends](/documentation/adding-a-backend) — 完整组装器接线 +- [Semantic Type](/documentation/semantic-types) — `channelSemantics` 包含的内容 +- [Auto Layout Algorithm](/documentation/layout-model) — `declareLayoutMode` 与 stretch 模型 +- [API reference](/documentation/api-reference) — `chart_spec.chartType` 与 encodings diff --git a/docs/zh-CN/adding-a-semantic-type.md b/docs/zh-CN/adding-a-semantic-type.md new file mode 100644 index 00000000..1ccefc91 --- /dev/null +++ b/docs/zh-CN/adding-a-semantic-type.md @@ -0,0 +1,128 @@ +# 扩展语义类型 + +语义类型是 LLM 和用户为字段附加的标签。当新的字段含义需要改变 Flint 如何格式化数值、聚合数据、选择比例尺或分配颜色时,应扩展语义类型。若 Flint 无法识别某类型,会优雅地回退到 `Unknown`。 + +完整类型层级与解析规则见 [Semantic Type](/documentation/semantic-types)。 + +--- + +## 目录 + +- [§1 判断是否需要新类型](#1-判断是否需要新类型) +- [§2 注册类型](#2-注册类型) +- [§3 同步常量与注解](#3-同步常量与注解) +- [§4 测试与验证](#4-测试与验证) +- [§5 相关文档](#5-相关文档) + +--- + +# §1 判断是否需要新类型 + +在添加类型之前,确认它与 T1 父类型相比**确实会改变编译行为**。避免注册同义词,例如 `Money`、`Price` 和 `Currency`;在注册表中选定一个名称,必要时在 agent 提示词中为其他名称做别名。 + +| 问题 | 若答案为是 | +|---|---| +| 是否已有 T2 类型以相同方式编译? | 使用该类型,并通过 `SemanticAnnotation` 元数据补充 | +| 该类型是否需要有界比例尺或单位? | 保留该类型;在注解中记录所需的 `intrinsicDomain` / `unit` | +| 是否仅为 agent 提供更友好的标签? | 优先使用 T1(`Amount`、`SignedMeasure`),而非新建 T2 | + +已弃用类型的说明与完整清单见 [Semantic Type §2.4](/documentation/semantic-types#24-tier-2-specific-types)。 + +--- + +# §2 注册类型 + +**单一事实来源:** `packages/flint-js/src/core/type-registry.ts` + +在 `TYPE_REGISTRY` 中添加一条记录。记录的键是 **T2 类型名称**,也是用户在 `semantic_types` 中传入的字符串。 + +```typescript +PercentageChange: { + t0: 'Measure', + t1: 'SignedMeasure', + visEncodings: ['quantitative'], + aggRole: 'signed-additive', + domainShape: 'open', + diverging: 'conditional', + formatClass: 'percent', + zeroBaseline: 'contextual', + zeroPad: 0.05, +}, +``` + +### `TypeRegistryEntry` 字段 + +| 字段 | 取值 | 驱动 | +|---|---|---| +| `t0` | `T0Family` | 解析器 / 编码族 | +| `t1` | `T1Category` | 中层规则选择 | +| `visEncodings` | `VisCategory[]`(优先级顺序) | 默认 Q/O/N/T 编码 | +| `aggRole` | `additive`, `intensive`, `signed-additive`, `dimension`, `identifier` | 通过 `resolveAggregationDefault()` 得到 `aggregationDefault` | +| `domainShape` | `open`, `bounded`, `fixed`, `cyclic` | 域约束、刻度、极坐标提示 | +| `diverging` | `none`, `conditional`, `inherent` | 发散色与中点 | +| `formatClass` | `currency`, `percent`, `unit-suffix`, `integer`, `decimal`, `plain` | 通过 `resolveFormat()` 设置轴/工具提示格式 | +| `zeroBaseline` | `meaningful`, `arbitrary`, `contextual`, `none` | Stage 4 中 `computeZeroDecision()` 的提示 | +| `zeroPad` | `number`(0–1 的小数) | 轴不包含零时的内边距 | + +查询 API:同文件中的 `getRegistryEntry()`、`isRegistered()`、`getRegisteredTypes()`。 + +**不在注册表中**(在其他位置解析):显式 `pattern` 字符串、轴反转、`colorScheme` 名称以及 `stackable`。这些来自 `field-semantics.ts` / `resolve-semantics.ts`,它们将注册表维度与数据和通道上下文结合。 + +--- + +# §3 同步常量与注解 + +### `SemanticTypes` 常量 + +在 `packages/flint-js/src/core/semantic-types.ts` 中添加对应键,以便调用点安全引用该类型: + +```typescript +export const SemanticTypes = { + // ... + PercentageChange: 'PercentageChange', +} as const; +``` + +### 字段级元数据(可选) + +**不属于**类型内在属性的逐字段细节应放在 `SemanticAnnotation`(`field-semantics.ts`)上,而非 `TYPE_REGISTRY`: + +```typescript +interface SemanticAnnotation { + semanticType: string; + intrinsicDomain?: [number, number]; // e.g. Rating [1, 5] + unit?: string; // e.g. USD, °C + sortOrder?: string[]; // custom ordinal order +} +``` + +图表输入接受 `Record` 作为 `semantic_types`。 + +--- + +# §4 测试与验证 + +1. **Gallery 用例** — 在 `packages/flint-js/src/test-data/semantic-tests.ts` 中添加或扩展生成器,在相关通道上使用新类型。 +2. **注册生成器** — 若这会创建新的 gallery 页面,在 `packages/flint-js/src/test-data/index.ts`(`TEST_GENERATORS`)中接线,并可选地在 `gallery-tree.ts` 中注册。 +3. **运行检查:** + +```bash +npm run typecheck +npm run test +npm run site # open Gallery → Semantic Context (or your new page) +``` + +验证: + +- 轴格式符合 `formatClass`(以及注解中的 `unit`) +- 在 `autoAggregate` 适用时,聚合遵循 `aggRole` +- 零基线与反转符合类型 + 标记(条形 vs 折线) +- 仅在 `diverging` 与数据需要时出现发散色 + +--- + +# §5 相关文档 + +- [Semantic Type](/documentation/semantic-types) — T0/T1/T2 层级、注解、解析规则 +- [Architecture](/documentation/architecture) — `resolveChannelSemantics` 在流水线中的位置 +- [API reference](/documentation/api-reference) — `ChartAssemblyInput` 上的 `semantic_types` diff --git a/docs/zh-CN/api-reference.md b/docs/zh-CN/api-reference.md new file mode 100644 index 00000000..bf9cee8d --- /dev/null +++ b/docs/zh-CN/api-reference.md @@ -0,0 +1,265 @@ +# API 参考 + +JavaScript / TypeScript 包:**`flint-chart`**(`packages/flint-js`)。 + +Python 移植:**`packages/flint-py`** 为源码预览。其输入形状与 JS API 一致,但 PyPI 发布计划在后续版本。 + +概念背景:[概览](/documentation/overview) · 流水线:[架构](/documentation/architecture) + +--- + +## 目录 + +- [§1 Flint 规范映射](#1-flint-spec-mapping) +- [§2 Assembler](#2-assemblers) +- [§3 ChartAssemblyInput](#3-chartassemblyinput) +- [§4 编码与选项](#4-encodings-and-options) +- [§5 完整示例](#5-complete-example) +- [§6 模板发现](#6-template-discovery) +- [§7 核心工具](#7-core-utilities) +- [§8 溢出与警告](#8-overflow-and-warnings) +- [§9 子路径导出](#9-subpath-exports) +- [§10 相关](#10-related) + +--- + +# §1 Flint 规范映射 + +| Flint | API 字段 | 内容 | +|---------------|-----------|----------| +| 原始表 | `data` | `{ values: rows[] }` 或 `{ url: "..." }` | +| **dataSpec** | `semantic_types` | `field → string` 或 `field → SemanticAnnotation` | +| **chartSpec** | `chart_spec` | `chartType`、`encodings`、`canvasSize`、`chartProperties` | + +每个数据集编写一次 `semantic_types`,并在多张图表间复用。探索阶段通常只有 `chart_spec` 会变化。 + +### SemanticAnnotation(内联于 `semantic_types`) + +```ts +interface SemanticAnnotation { + semanticType: string; + intrinsicDomain?: [number, number]; // e.g. Rating [1, 5] + unit?: string; // e.g. USD, °C + sortOrder?: string[]; // custom ordinal order +} +``` + +裸字符串简写:`"Price"` 等价于 `{ semanticType: "Price" }`。 + +--- + +# §2 Assembler + +所有后端接受相同的 `ChartAssemblyInput`,并返回可直接渲染的对象。 + +```ts +import { + assembleVegaLite, + assembleECharts, + assembleChartjs, +} from 'flint-chart'; + +const vlSpec = assembleVegaLite(input); +const ecSpec = assembleECharts(input); +const cjsSpec = assembleChartjs(input); +``` + +| 导出 | 返回 | +|--------|---------| +| `assembleVegaLite` | Vega-Lite JSON spec | +| `assembleECharts` | ECharts `option` object | +| `assembleChartjs` | Chart.js configuration | + +若某后端不支持某 `chartType`,assembler 会在渲染前抛出。可用 `vlGetTemplateDef`、`ecGetTemplateDef` 或 `cjsGetTemplateDef` 检查支持情况。 + +--- + +# §3 ChartAssemblyInput + +```ts +interface ChartAssemblyInput { + data: { values: Record[] } | { url: string }; + semantic_types?: Record; + chart_spec: { + chartType: string; + 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 + chartProperties?: Record; + }; + options?: AssembleOptions; + field_display_names?: Record; +} +``` + +### `data` + +| 形式 | 说明 | +|------|-------------| +| `{ values: rows[] }` | 内联行对象(编辑器与教程) | +| `{ url: "..." }` | 远程 JSON 或 CSV URL | + +### `semantic_types` + +将列名映射到语义类型。这驱动编码类型、格式化、聚合默认值、颜色类与布局。见[语义类型](/documentation/semantic-types)。 + +### `chart_spec` + +| 字段 | 说明 | +|-------|-------------| +| `chartType` | 模板名称 — 须与后端注册表项匹配(`"Bar Chart"`、`"Heatmap"` 等) | +| `encodings` | 通道 → 编码映射 | +| `baseSize` | **目标**布局尺寸(像素,默认 400×320):典型数据下图表瞄准的尺寸。密集数据可能超出,直至上限。 | +| `canvasSize` | **硬上限:** 图表可达到的最大尺寸,含分面网格。若省略,上限为 `baseSize × options.maxStretch`(默认 1.5×)。各维度上限为 `βx = canvasSize.width / baseSize.width`、`βy = canvasSize.height / baseSize.height`(均 ≥ 1)。基准会被钳制到上限,因此单独设置 `canvasSize` 即相当于固定框,图表会填充并缩小以适配而不溢出。 | +| `chartProperties` | 模板特定开关(例如 `orient`、`opacity`) | + +> **base 与 canvas,一句话:** `baseSize` 是图表*瞄准*的尺寸;`canvasSize` 是*绝不超过*的尺寸。固定插槽用 `canvasSize`,舒适目标且密集数据可增长用 `baseSize`。见[示例:自动布局](/documentation/chart-sizing)。 + +--- + +# §4 编码与选项 + +### ChartEncoding + +```ts +interface ChartEncoding { + field?: string; + type?: 'quantitative' | 'nominal' | 'ordinal' | 'temporal'; + aggregate?: 'count' | 'sum' | 'average' | 'mean'; + sortOrder?: 'ascending' | 'descending'; + sortBy?: string; + scheme?: string; +} +``` + +显式 `type` 覆盖语义推断。设置 `aggregate` 表示由 Flint 自行折叠行——按其他(未聚合)字段通道分组,并产生名为 `${field}_${aggregate}` 的派生列(`count` → `_count`)。`average` 与 `mean` 同义。多数调用方仍应在上游聚合数据;若已聚合,省略 `aggregate` 并按列名引用派生列。 + +常见通道:`x`、`y`、`color`、`size`、`shape`、`column`、`row`、`group`、`detail`。 + +### AssembleOptions(节选) + +```ts +interface AssembleOptions { + addTooltips?: boolean; // default false + elasticity?: number; // discrete stretch exponent (default 0.5) + maxStretch?: number; // default stretch cap when no canvasSize ceiling (default 1.5) + maxStretchX?: number; // per-dimension width cap (derived from canvasSize) + maxStretchY?: number; // per-dimension height cap (derived from canvasSize) + facetElasticity?: number; // facet stretch (default 0.3) + minStep?: number; // min px per discrete item (default 6) + minSubplotSize?: number; // min facet subplot px (default 60) + maxColorValues?: number; // color cardinality before truncation (default 24) + stepPadding?: number; // band inner padding fraction (default 0.1) + defaultBandSize?: number; // baseline px per category (backend-tuned) +} +``` + +完整列表:`packages/flint-js/src/core/types.ts`(`AssembleOptions`)。行为见[自动布局算法](/documentation/layout-model)。 + +--- + +# §5 完整示例 + +```ts +const input: ChartAssemblyInput = { + data: { + values: [ + { quarter: 'Q1', revenue: 1200 }, + { quarter: 'Q2', revenue: 1450 }, + { quarter: 'Q3', revenue: 980 }, + { quarter: 'Q4', revenue: 1800 }, + ], + }, + semantic_types: { quarter: 'Quarter', revenue: 'Price' }, + chart_spec: { + chartType: 'Bar Chart', + encodings: { + x: { field: 'quarter' }, + y: { field: 'revenue' }, + }, + baseSize: { width: 480, height: 320 }, + }, +}; + +const spec = assembleVegaLite(input); +``` + +--- + +# §6 模板发现 + +```ts +import { + vlTemplateDefs, + vlGetTemplateDef, + vlGetTemplateChannels, + ecGetTemplateDef, + cjsGetTemplateDef, +} from 'flint-chart'; + +Object.keys(vlTemplateDefs); +// ["Points", "Bars", "Lines & Areas", …] + +vlGetTemplateChannels('Scatter Plot'); +// ["x", "y", "color", "size", "opacity", "column", "row"] +``` + +--- + +# §7 核心工具 + +从 `flint-chart` 与 `flint-chart/core` 再导出: + +| 符号 | 用途 | +|--------|---------| +| `inferVisCategory` | 从原始数据推断粗粒度可视化类别 | +| `getVisCategory` | 按语义类型字符串查找类别 | +| `getRegistryEntry` | 查询类型的 `TypeRegistryEntry` | +| `channels`、`channelGroups` | 通道元数据 | + +关键类型:`ChartAssemblyInput`、`ChartEncoding`、`ChartTemplateDef`、`AssembleOptions`、`ChartWarning`、`ChannelSemantics`。 + +--- + +# §8 溢出与警告 + +当离散通道超出布局预算时,编译器会: + +1. 计算可容纳多少项([自动布局算法 §2](/documentation/layout-model#2-discrete-axis-elastic-budget-model)) +2. 应用模板溢出策略 +3. 将数据过滤为保留值 +4. 将警告附加到结果 + +默认策略优先级: + +1. 连接标记(折线、面积)— 保留所有点 +2. 用户指定排序 — 保留前/后 N 项 +3. 对侧定量轴 — 排序并截断 +4. 柱图 + count — 先 sum 聚合再截断 +5. 数值字段 — 数值排序,取前 N +6. 回退 — 按数据顺序取前 N + +在集成代码中检查 `_warnings` 或 `ChartWarning` 数组,以便在 UI 中展示截断信息。 + +--- + +# §9 子路径导出 + +| 导入路径 | 内容 | +|-------------|----------| +| `flint-chart` | Assembler + 主要再导出 | +| `flint-chart/core` | 类型、语义、布局 | +| `flint-chart/vegalite` | VL 模板与 `assembleVegaLite` | +| `flint-chart/echarts` | ECharts 模板与 `assembleECharts` | +| `flint-chart/chartjs` | Chart.js 模板与 `assembleChartjs` | +| `flint-chart/test-data` | 图库生成器(`TEST_GENERATORS`) | + +--- + +# §10 相关 + +- [概览](/documentation/overview) — dataSpec + chartSpec 动机 +- [架构](/documentation/architecture) — 三阶段流水线 +- [语义类型](/documentation/semantic-types) — 类型层次与解析 +- [入门指南](/documentation/getting-started) — 动手演练 +- [扩展后端](/documentation/adding-a-backend) — 新 `assemble*()` 目标 diff --git a/docs/zh-CN/architecture.md b/docs/zh-CN/architecture.md new file mode 100644 index 00000000..70c237c0 --- /dev/null +++ b/docs/zh-CN/architecture.md @@ -0,0 +1,195 @@ +# 架构 + +Flint 是一种与库无关的可视化中间语言。每个 `assemble*()` 入口都使用相同的**编译器前端**和**优化器**;仅**代码生成器**因后端而异。 + +动机与规范示例见[概览](/documentation/overview)。输入类型见 [API 参考](/documentation/api-reference)。 + +--- + +## 目录 + +- [§1 设计原则](#1-design-principles) +- [§2 三阶段流水线](#2-three-stage-pipeline) +- [§3 阶段 1 — 编译器前端](#3-stage-1-compiler-frontend) +- [§4 阶段 2 — 优化器](#4-stage-2-optimizer) +- [§5 阶段 3 — 代码生成器](#5-stage-3-code-generator) +- [§6 输入](#6-inputs) +- [§7 溢出与警告](#7-overflow-and-warnings) +- [§8 仓库布局](#8-repository-layout) +- [§9 相关](#9-related) + +--- + +# §1 设计原则 + +1. **语义优先** — `semantic_types` 指导解析、聚合、零基线、发散检测和格式化。原始存储类型只是起点。 +2. **最小图表表面** — `chart_spec` 提供图表类型与通道绑定,通常约 10 行。坐标轴、比例尺、图例和步长由编译器推导。 +3. **动态模板** — 每个 `chartType` 映射到 `ChartTemplateDef`;其 `instantiate()` 钩子消费完整编译上下文,并适应基数与语义。 +4. **无 UI 依赖** — 核心为纯 TypeScript(`packages/flint-js`),可从智能体、笔记本、服务器或本站运行。Python 包计划在后续版本发布。 + +大部分设计逻辑位于阶段 1–2,且在各后端间完全相同。 + +--- + +# §2 三阶段流水线 + +![Overview of the Flint architecture](figs/overview.png) + +| 阶段 | 作用 | 实现 | 关键输出 | +|-------|------------|----------------|-------------| +| **1. 编译器前端** | 解析语义上下文 | Phase 0 — `resolveChannelSemantics()` | 每通道 `ChannelSemantics` | +| **2. 优化器** | 将布局适配画布 | Phase 1 — `computeLayout()`、`filterOverflow()` | `LayoutResult`、截断数据 | +| **3. 代码生成器** | 输出库原生规范 | Phase 2 — `build*Encodings()`、`template.instantiate()` | VL / EC / CJS spec | + +```text +assembleVegaLite(input) // or assembleECharts, assembleChartjs + │ + ▼ +══ STAGE 1 — COMPILER FRONTEND (core/) ═════════════════════════ + │ + ├── resolveChannelSemantics() semantic_types + data → ChannelSemantics + ├── computeZeroDecision() per quantitative axis (needs template mark) + ├── declareLayoutMode() template layout intent (optional) + └── convertTemporalData() semantic-driven date parsing + │ + ▼ +══ STAGE 2 — OPTIMIZER (core/) ══════════════════════════════════ + │ + ├── computeChannelBudgets() + filterOverflow() + └── computeLayout() + • Discrete axes — elastic budget (bars, heatmap cells) + • Continuous axes — gas-pressure stretch (scatter, line) + • Global — facet grid, aspect ratio, radial / area models + │ + ▼ +══ STAGE 3 — CODE GENERATOR (per backend) ═══════════════════════ + │ + ├── build*Encodings() backend encoding objects + ├── template.instantiate() dynamic template hook + ├── restructureFacets() VL / ECharts faceting + └── applyLayoutToSpec() step, width/height, padding + │ + ▼ + Native spec + optional warnings +``` + +规范编排:`packages/flint-js/src/vegalite/assemble.ts`。 + +--- + +# §3 阶段 1 — 编译器前端 + +解析分两层;完整流水线见[语义类型 §4](/documentation/semantic-types#4-compilation-pipeline)。 + +### 字段属性 + +按列、与图表无关:格式类、聚合角色、域形状、发散提示、规范顺序。由 `type-registry.ts` 与可选内联注解(`intrinsicDomain`、`unit`、`sortOrder`)驱动。 + +### 通道属性 + +图表上下文落地。同一 `YearMonth` 字段在折线图的 `x` 上可能是时间型,在另一视图的 `color` 上可能是分类型。通道语义防止年月整数被当作定量幅度。 + +**IR:** `ChannelSemantics` — 扁平、与后端无关的记录,供布局与所有模板消费。 + +分层类型(T0 → T1 → T2)在智能体提供粗粒度标签时允许优雅降级。 + +### 命名视图变换 + +Flint 将部分替代方案暴露为**命名视图**,而非要求用户或智能体重写图表规范。命名视图是对编码分配的小型变换:翻转坐标轴、将分类轴与颜色系列交换、将系列路由到分面,或以兄弟图表类型重新渲染相同字段。宿主仅在 `chart_spec.chartProperties.pivot` 中存储所选状态 id;编译器重新计算得到的编码映射,并在该视图上运行常规的语义、溢出、布局与后端生成流水线。 + +该模型在群论意义上成立,但刻意保持实用。从作者分配 `a0` 出发,沿四个算子生成的轨道遍历: + +| 符号 | 生成元 | 示例状态 id | 含义 | +|--------|-----------|------------------|---------| +| `τ` | transpose | `flip:x-y` | 整体翻转两个轴槽位,保持占用关系 | +| `σ` | permute | `swap:y-color` | 将位置字段与同配置的辅助通道交换 | +| `γ` | shift | `series:row` | 在 color/group/facet 通道间路由一个离散系列字段 | +| `θ` | transition | `type:Strip Plot` | 用兄弟模板重新渲染相同路由字段 | + +可见的 View 控件是有效性检查与去重后的有限轨道。去重是稳定子群的具象形式:翻转两次回到 `Default`,先分面再抖动可能与直接抖动坍缩为同一 Strip Plot,Scatter → Strip Plot → Scatter 这类图表类型往返会折叠回作者编写的散点图。兼容性检查也有类型约束:`σ` 仅在同一字段配置内交换(度量与度量、类别与类别),而 `τ` 允许跨配置,因为它翻转的是轴槽位而非字段角色。折线图省略 `τ`,因此 Flint 从不提供垂直折线图。 + +由于轨道在 Flint 的后端中立编码 IR 上计算,相同的 View 状态 id 适用于 Vega-Lite、ECharts 和 Chart.js。各后端接收已变换的编码映射;仅 `θ` 需要后端特定的模板查找,以便兄弟图表自身的实例化逻辑接管。 + +--- + +# §4 阶段 2 — 优化器 + +优化器接收 `baseSize`(目标)和可选的 `canvasSize` 上限,然后产生在可用空间内保持图表可读的 `LayoutResult`。 + +### 局部优化 + +每个布局维度(x、y、group、分面列/行、radius)都是弹性容器: + +| 编码类 | 行为 | +|----------------|----------| +| 离散(柱、热力图单元格) | 向最小可读步长压缩;必要时拉伸画布 | +| 连续(散点、折线) | 标记密度超过重叠预算时拉伸 | + +### 全局优化 + +宽高比(连接标记的 banking-to-45°)、分面行列换行,以及由组件数量确定尺寸的非笛卡尔图表(treemap、gauge、pie)。 + +实现模型:[自动布局算法](/documentation/layout-model) — §2 弹性预算、§3 气压、§4 周长、§5 面积。 + +--- + +# §5 阶段 3 — 代码生成器 + +后端生成器将优化后的上下文翻译为库原生语法。每个 `chartType` 注册一个**动态模板**: + +| `ChartTemplateDef` 字段 | 作用 | +|--------------------------|------| +| `chart` | 公开名称(`"Grouped Bar Chart"`)— 与 `chart_spec.chartType` 匹配 | +| `template` | 原生规范骨架 | +| `channels` | 允许的编码 | +| `markCognitiveChannel` | `position` / `length` / `area` / `color` — 零基线与拉伸类 | +| `declareLayoutMode?` | 布局前的轴标志 | +| `instantiate()` | 从 `InstantiateContext` 输出规范 | + +注册表:`vlTemplateDefs`、`ecTemplateDefs`、`cjsTemplateDefs`。查找:`vlGetTemplateDef(name)` 等。 + +新后端仅实现阶段 3;前端与优化器保持不变。见[扩展后端](/documentation/adding-a-backend)。 + +--- + +# §6 输入 + +| 部分 | API | 指定内容 | +|------|-----|-----------| +| 原始数据 | `data` | 供解析器与布局使用的行表 | +| **dataSpec** | `semantic_types` | 字段含义;在同一数据集上的多张图表间复用 | +| **chartSpec** | `chart_spec` | `chartType` + `encodings`;探索时易于编辑 | + +LLM 智能体通常一次性推断 `semantic_types`,然后迭代 `chart_spec`:在同一语义层上从折线 → 热力图 → 分组柱 → 瀑布 → 旭日图。 + +--- + +# §7 溢出与警告 + +当离散基数超过画布预算时,优化器会过滤数据并附加 `ChartWarning` 元数据,而不是渲染不可读的图表。策略优先级与 `_warnings` 检查见 [API 参考 §8](/documentation/api-reference#8-overflow-and-warnings)。 + +--- + +# §8 仓库布局 + +```text +packages/flint-js/src/ +├── core/ resolve-semantics, field-semantics, compute-layout, type-registry, types +├── vegalite/ Stage 3 — Vega-Lite templates + assembleVegaLite +├── echarts/ Stage 3 — ECharts templates + assembleECharts +├── chartjs/ Stage 3 — Chart.js templates + assembleChartjs +└── test-data/ gallery fixtures (TEST_GENERATORS) + +packages/flint-py/ Python port preview (package planned later) +site/ demo site (gallery, editor, documentation) +``` + +--- + +# §9 相关 + +- [概览](/documentation/overview) — 动机与规范示例 +- [API 参考](/documentation/api-reference) — `ChartAssemblyInput`、assembler、options +- [语义类型](/documentation/semantic-types) — 类型层次与解析规则 +- [自动布局算法](/documentation/layout-model) — 拉伸与分面模型 +- [扩展图表模板](/documentation/adding-a-chart-template) — 扩展阶段 3 diff --git a/docs/zh-CN/color-decisions.md b/docs/zh-CN/color-decisions.md new file mode 100644 index 00000000..447bf5c8 --- /dev/null +++ b/docs/zh-CN/color-decisions.md @@ -0,0 +1,207 @@ +# 颜色决策 + +Flint 将**使用何种颜色比例尺**(分类型、顺序型或发散型)与**各后端如何渲染**分离。颜色逻辑分两层运行: + +1. **Phase 0 — 语义解析** 根据字段语义类型与数据,为每个通道分配 `ChannelSemantics.colorScheme` 建议(Vega-Lite 直接使用)。 +2. **`decideColorMaps()`**(位于 `core/color-decisions.ts`)将语义 + 编码转为与后端无关的 `ColorDecision` 记录(ECharts 与 Chart.js 使用,随后在本地选取具体十六进制调色板)。 + +两层都不输出 Vega-Lite 或 ECharts 语法——仅输出抽象方案*类型*、可选显式方案 id、类别数量与发散中点。 + +--- + +## 在流水线中的位置 + +``` +resolveChannelSemantics() Phase 0 + └── cs.colorScheme { type, scheme, domainMid? } + │ + ├─► Vega-Lite assemble + │ buildVLEncodings() copies scheme → encoding.scale.scheme + │ (VL built-in scheme names: category10, viridis, redblue, …) + │ + └─► ECharts / Chart.js assemble + decideColorMaps() → ColorDecisionResult + └── pickEChartsPalette() / pickChartJsPalette() + └── hex color arrays on series / legend +``` + +| 后端 | 颜色入口 | 调色板来源 | +|---------|-------------------|----------------| +| Vega-Lite | `vegalite/assemble.ts` 中的 `ChannelSemantics.colorScheme` | `getRecommendedColorScheme()` → [Vega scheme names](https://vega.github.io/vega/docs/schemes/) | +| ECharts | `decideColorMaps()` → `context.colorDecisions` | `echarts/colormap.ts` — `cat10`、`cat20`、`viridis`、`RdBu` | +| Chart.js | `decideColorMaps()` → `context.colorDecisions` | `chartjs/colormap.ts` — 相同 id,Chart.js 调优的十六进制值 | + +ECharts 与 Chart.js 在组装期间调用一次 `decideColorMaps()`,并将结果挂到 `InstantiateContext.colorDecisions`。模板与 `instantiate-spec.ts` 读取该对象;它们不会重新推导方案族。 + +--- + +## Phase 0:语义颜色提示 + +在 `resolveChannelSemantics()` 期间,承载颜色的通道(`color`,有时还有 `group`)会在 `ChannelSemantics` 上获得 `colorScheme`: + +```ts +interface ColorSchemeRecommendation { + scheme: string; // e.g. 'tableau10', 'viridis', 'redblue' + type: 'categorical' | 'sequential' | 'diverging'; + domainMid?: number; + reason?: string; +} +``` + +生产路径: + +1. `resolveColorSchemeHint(semanticType, annotation, values)` — 根据类型注册表与数据范围分类发散型、顺序型与分类型。 +2. `getRecommendedColorScheme(...)`(位于 `core/semantic-types.ts`)— 从内部 `colorSchemes` 注册表选取具体 Vega-Lite 方案名。 + +示例: + +| 语义类型 | 典型提示 | 示例方案 | +|---------------|--------------|----------------| +| `Country`、`Category` | categorical | 按基数选 `tableau10` / `tableau20` | +| `Quantity`、`Temperature` | sequential | `viridis`、`reds` 等 | +| `Percentage`、`Correlation`(跨 ±) | diverging | `redblue` 与 `domainMid` | + +Vega-Lite 编码构建随后应用: + +- 若用户设置了 `encoding.scheme` 且不为 `'default'`,则使用该值,否则 +- 对发散比例尺使用 `cs.colorScheme.scheme` 与 `domainMid`。 + +类型如何馈入这些提示见[语义类型](/documentation/semantic-types)。 + +--- + +## 核心:`decideColorMaps()` + +**文件:** `packages/flint-js/src/core/color-decisions.ts` + +### 输入 + +```ts +interface DecideColorMapsContext { + chartType: string; + encodings: Record; + channelSemantics: Record; + table: any[]; + background?: 'light' | 'dark'; // reserved +} +``` + +### 输出 + +```ts +interface ColorDecisionResult { + color?: ColorDecision; + group?: ColorDecision; + fill?: ColorDecision; // reserved + stroke?: ColorDecision; // reserved +} + +interface ColorDecision { + channel: 'color' | 'group' | 'fill' | 'stroke'; + schemeType: 'categorical' | 'sequential' | 'diverging'; + schemeId?: string; // set when user passes encoding.scheme + divergingMidpoint?: number; + categoryCount?: number; // distinct values in the color field + primary: boolean; // true for color / group + dataDriven: boolean; +} +``` + +仅有绑定字段的通道会得到决策。当前评估 **`color` 与 `group`**;`fill` / `stroke` 为保留。 + +### 每通道算法 + +`decideColorForChannel()` 按顺序执行: + +1. **显式方案** — 若设置了 `encoding.scheme` 且不为 `'default'`,则透传 `schemeId`。从 `ChannelSemantics` 推断 `schemeType`(core 不校验 id;后端在其注册表中查找)。 + +2. **语义驱动类型** — `decideSchemeTypeFromChannel()` 读取 `cs.colorScheme` 与编码/语义上下文: + +| 条件 | `schemeType` | +|-----------|--------------| +| `colorScheme.type === 'diverging'` | `diverging`(+ `domainMid` 作为中点) | +| `colorScheme.type === 'sequential'` | `sequential` | +| `colorScheme.type === 'categorical'` + 语义 `Rank` | `sequential`(在连续色带上表示秩) | +| `colorScheme.type === 'categorical'` + `color` 上 `temporal` | `sequential`(避免将日期当作离散类别) | +| `colorScheme.type === 'categorical'`(默认) | `categorical` | +| 无提示 + 语义 `Correlation` | `diverging`,中点 `0` | +| 无提示 + 编码 `quantitative` 或 `temporal` | `sequential` | +| 回退 | `categorical` | + +3. **基数** — `countDistinctValues(table, field)` → `categoryCount`,供后端调色板尺寸使用(例如 `cat10` 与 `cat20`)。 + +Core 在自动路径上**有意不**选取默认 `schemeId`。除非用户覆盖了 `scheme`,后端根据 `schemeType` + `categoryCount` 选择调色板。 + +--- + +## 后端调色板注册表 + +### ECharts — `echarts/colormap.ts` + +内置映射:`cat10`、`cat20`、`viridis`、`RdBu`。每项包含 `type`、`supportsDiscrete`、`supportsContinuous`、`maxCategories`、`colorblindSafe` 与 `colors: string[]` 数组。 + +**`pickEChartsPalette(decision)`** + +1. 若设置了 `decision.schemeId` → `getPaletteForScheme(id)`。 +2. 否则按 `decision.schemeType` 过滤映射: + - **categorical** — 最小的 `maxCategories` ≥ `categoryCount`(优先 `cat10` / `cat20`)。 + - **sequential** — 首个支持连续的映射(通常为 `viridis`)。 + - **diverging** — `diverging: true` 的映射(通常为 `RdBu`)。 +3. 回退 → ECharts 模板的 `DEFAULT_COLORS`。 + +**`getPaletteForScheme(id)`** — 按 id 查找(不区分大小写);模板(Treemap、Heatmap、Graph 等)在需要直接取色时使用。 + +### Chart.js — `chartjs/colormap.ts` + +结构与选择策略与 ECharts 相同,十六进制默认值为 Chart.js 调优。回退 → `cat10`。 + +### Vega-Lite + +不调用 `decideColorMaps()`。方案为 Phase 0 解析的**名称**(`category10`、`tableau20`、`viridis`、`redblue` 等),在 `buildVLEncodings()` 期间写入 `encoding.scale.scheme`。 + +--- + +## 用户覆盖 + +在 `chart_spec.encodings` 的任意颜色编码上设置 `scheme`: + +```json +"encodings": { + "color": { "field": "region", "scheme": "viridis" } +} +``` + +| 后端 | 效果 | +|---------|--------| +| Vega-Lite | `scale.scheme = "viridis"`(Vega 内置名) | +| ECharts / Chart.js | `ColorDecision.schemeId = "viridis"` → 在后端注册表中查找调色板 | + +为在 ECharts 与 Chart.js 间获得可移植结果,请使用目标后端注册表中存在的 id(`cat10`、`viridis`、`RdBu` 等)。Vega-Lite 接受更广的 [Vega 方案目录](https://vega.github.io/vega/docs/schemes/)。 + +--- + +## 设计理由 + +**决策与渲染分离** — 方案*族*与基数在 core(或 Vega-Lite 的 Phase 0)中一次性决定。十六进制数组与比例尺对象留在后端代码中,因此 ECharts 与 Chart.js 可在视觉上分叉而不复制语义逻辑。 + +**共享语义** — 同一 `ChartAssemblyInput` 在 ECharts 与 Chart.js 上产生相同的 `schemeType` 与 `categoryCount`。仅每后端主题的调色板十六进制值不同。 + +**优雅路径** — 显式 `encoding.scheme` 优先。否则语义与编码类型驱动方案族;后端始终有回退调色板。 + +### 扩展点 + +| 变更 | 位置 | +|--------|--------| +| 新语义 → 方案规则 | `getRecommendedColorScheme()` / `resolveColorSchemeHint()` | +| 新方案族规则 | `decideSchemeTypeFromChannel()` | +| 新 ECharts / Chart.js 调色板 | `ECHARTS_COLOR_MAPS` / `CHARTJS_COLOR_MAPS` | +| 新颜色通道 | `ColorChannel` 联合类型 + `decideColorMaps()` 中的循环 | +| Vega-Lite 统一路径 | 在 `vegalite/assemble.ts` 中调用 `decideColorMaps()`(当前未接入) | + +--- + +## 相关 + +- [语义类型](/documentation/semantic-types) — 类型注册表与 Phase 0 中的 `colorScheme` 提示 +- [架构](/documentation/architecture) — 完整编译流水线 +- [API 参考](/documentation/api-reference) — `ChartEncoding.scheme` diff --git a/docs/zh-CN/design-semantics.md b/docs/zh-CN/design-semantics.md new file mode 100644 index 00000000..5d50222f --- /dev/null +++ b/docs/zh-CN/design-semantics.md @@ -0,0 +1,863 @@ +# 语义类型 + +语义类型描述每个数据字段*代表什么*,而不仅仅是如何存储。它们告诉 Flint 字段应如何编码、格式化、聚合、排序和着色。编译器首先将每个字段的语义类型及可选注解解析为 `FieldSemantics`,然后将相关决策提升为各通道的 `ChannelSemantics`,用于布局和后端规范生成。 + +--- + +## 目录 + +- [§1 概览](#1-overview) +- [§2 类型层级](#2-type-hierarchy) + - [§2.1 分层类型系统](#21-tiered-type-system) + - [§2.2 第 0 层 — 族(Family)](#22-tier-0-families) + - [§2.3 第 1 层 — 类别(Category)](#23-tier-1-categories) + - [§2.4 第 2 层 — 具体类型](#24-tier-2-specific-types) + - [§2.5 层级作为 DAG](#25-the-hierarchy-as-a-dag) + - [§2.6 循环域类型](#26-cyclic-domain-types) + - [§2.7 LLM 注解策略](#27-llm-annotation-strategies) + - [§2.8 类型注册表](#28-type-registry) +- [§3 字段注解](#3-field-annotations) + - [§3.1 为何元数据重要](#31-why-metadata-matters) + - [§3.2 SemanticAnnotation](#32-semanticannotation) + - [§3.3 哪些类型需要元数据?](#33-which-types-need-metadata) + - [§3.4 数值表示检测](#34-numeric-representation-detection) + - [§3.5 接受字符串或对象](#35-accepting-string-or-object) +- [§4 编译流水线](#4-compilation-pipeline) + - [§4.1 四阶段概览](#41-four-stage-overview) + - [§4.2 字段与通道职责](#42-field-vs-channel-responsibilities) + - [§4.3 resolveFieldSemantics](#43-resolvefieldsemantics) + - [§4.4 resolveChannelSemantics](#44-resolvechannelsemantics) + - [§4.5 FieldSemantics 接口](#45-fieldsemantics-interface) + - [§4.6 ChannelSemantics 接口](#46-channelsemantics-interface) + - [§4.7 辅助类型](#47-supporting-types) + - [§4.8 布局与规范生成](#48-layout-and-spec-generation) + - [§4.9 缓存](#49-caching) +- [§5 解析规则](#5-resolution-rules) + - [§5.1 格式与解析](#51-format-and-parsing) + - [§5.2 聚合默认值](#52-aggregation-defaults) + - [§5.3 比例尺、域与刻度](#53-scale-domain-and-ticks) + - [§5.4 坐标轴与标记](#54-axes-and-marks) + - [§5.5 发散与颜色](#55-diverging-and-color) +- [§6 示例](#6-examples) + - [§6.1 收入柱状图](#61-revenue-bar-chart) + - [§6.2 温度折线图](#62-temperature-line-chart) + - [§6.3 排名 bump 图](#63-rank-bump-chart) + - [§6.4 带域的 Rating](#64-rating-with-domain) +- [§7 相关文档](#7-related) + +--- + +# §1 概览 + +**语义类型**是一个命名标签,例如 `Revenue`、`Month` 或 `Rating`,用于告诉编译器如何处理字段。类型组织为三个层级,对应 Flint 的语义级别: + +| Flint 级别 | 代码层级 | 数量 | 决定内容 | +|-------------|-----------|-------|---------| +| L1 语义域 | **T0** Family | 6 | 解析器类别、编码族(temporal / measure / categorical / …) | +| L2 语义族 | **T1** Category | 17 | 聚合角色、零点类别、格式类别、发散提示 | +| L3 语义类型 | **T2** Specific | 46 | 精确格式、域、刻度策略、类型特定呈现 | + +LLM 或用户可在任意层级进行注解。结果会优雅降级而非失败:`Revenue`(T2)产生货币格式、求和聚合和对数比例尺提示;`Amount`(T1)仍获得货币类别和求和;`Measure`(T0)仍获得定量编码和有意义零点,但无格式前缀。 + +**设计原则:** + +1. **语义类型是单一事实来源。** 编译上下文是(semanticType、dataValues、channel、markType)的确定性函数。无隐藏状态。 +2. **决策结构化,而非分散。** 一个构建器产生类型化上下文对象;下游代码读取这些对象,而非重新检查语义类型字符串。 +3. **先按字段,再按通道。** 格式和聚合是字段内在的;零点基线、反转和配色方案取决于通道和标记。 +4. **易于覆盖。** 每个决策都有类型推导的默认值;用户、模板或智能体可显式覆盖单个字段。 +5. **后端无关。** 上下文在翻译为 Vega-Lite、ECharts 或其他目标之前,描述抽象意图,如货币格式或反转坐标轴。 +6. **语义类型 + 可选元数据。** 有界比例尺、单位和自定义排序可能需要与类型字符串并行的结构化注解。 + +关于语义解析在完整编译路径中的位置,请参阅[架构](/documentation/architecture)。 + +--- + +# §2 类型层级 + +## §2.1 分层类型系统 + +不同任务需要不同粒度的 specificity。三个层级让 LLM 选择合适的花费/质量权衡: + +| 层级 | 数量 | 用途 | LLM 成本 | 可视化配置质量 | +|---|---|---|---|---| +| **T0 — Family** | 6 | 最粗:编码类型和基本默认值 | 最低 — 可基于规则回退 | 正确编码,通用格式化 | +| **T1 — Category** | 17 | 格式类别、聚合默认、零点基线、颜色类别 | 中等 — 小型封闭列表 | 良好格式化,合理默认值 | +| **T2 — Specific** | 46 | 发散中点、域约束、刻度策略 | 较高 — 更大词汇表 | 完整编译上下文 | + +## §2.2 第 0 层 — 族(Family) + +由启发式推断的宽泛类别(无需 LLM): + +| T0 Family | 数据类型 | 默认可视化编码 | 决定内容 | +|---|---|---|---| +| **Temporal** | date/string | temporal | 时间轴、日期解析、时间排序 | +| **Measure** | number | quantitative | 数值轴、aggregation=sum、有意义零点 | +| **Discrete** | number | ordinal | 整数刻度、无聚合、任意零点 | +| **Geographic** | number/string | geographic/nominal | 地图图层、地理编码 | +| **Categorical** | string | nominal | 颜色/形状/分面、无轴排序 | +| **Identifier** | number/string | nominal | 仅 tooltip,永不编码到轴/颜色 | + +T0 单独即可提供正确编码、基本聚合和零点基线类别。它无法捕获格式前缀/后缀、特定聚合、发散检测、域约束或比例尺提示。 + +## §2.3 第 1 层 — 类别(Category) + +每个 T1 精确映射到一个 T0 family: + +| T0 Family | T1 Categories | T1 相对 T0 的增量 | +|---|---|---| +| **Temporal** | `DateTime`, `DateGranule`, `Duration` | 时点 vs 粒度 vs 跨度;temporal vs ordinal 编码 | +| **Measure** | `Amount`, `Physical`, `Proportion`, `SignedMeasure`, `GenericMeasure` | 格式类别($、%、°)、聚合、发散检测 | +| **Discrete** | `Rank`, `Score`, `Index` | 反转轴(Rank)、整数刻度、域提示 | +| **Geographic** | `GeoCoordinate`, `GeoPlace` | 经纬度配对 vs 可地理编码名称 | +| **Categorical** | `Entity`, `Coded`, `Binned` | 基数期望、分箱的顺序性 | +| **Identifier** | `ID` | 永不聚合、永不编码 | + +**完整 T1 表:** + +| T1 Type | T0 Family | 可视化编码 | 决定内容 | +|---|---|---|---| +| `DateTime` | Temporal | temporal | 完整日期/时间解析、时间轴 | +| `DateGranule` | Temporal | ordinal or temporal | Month/Year/Quarter — 顺序排序、规范顺序 | +| `Duration` | Temporal | quantitative | 时间跨度格式化、sum/avg 聚合 | +| `Amount` | Measure | quantitative | 货币前缀、sum、有意义零点 | +| `Physical` | Measure | quantitative | 单位后缀、avg 聚合、Temperature 的任意零点 | +| `Proportion` | Measure | quantitative | % 格式化、有界域、avg 聚合 | +| `SignedMeasure` | Measure | quantitative | 发散中点(0)、有符号数据 | +| `GenericMeasure` | Measure | quantitative | 无特殊格式,sum/avg 来自字段名 | +| `Rank` | Discrete | ordinal | 反转轴、整数刻度、不可聚合 | +| `Score` | Discrete | quantitative | 有界域、整数刻度、avg 聚合 | +| `Index` | Discrete | ordinal/nominal | 行号 — 不可聚合 | +| `GeoCoordinate` | Geographic | quantitative | 固定域(lat/lon)、地图投影 | +| `GeoPlace` | Geographic | nominal | 可地理编码名称、choropleth/symbol-map | +| `Entity` | Categorical | nominal | 高基数、适合 tooltip | +| `Coded` | Categorical | nominal | 低基数、离散颜色(Status、Type、Boolean、Direction) | +| `Binned` | Categorical | ordinal | 预分箱区间、顺序轴 | +| `ID` | Identifier | nominal | 永不聚合、仅 tooltip | + +## §2.4 第 2 层 — 具体类型 + +每个 T2 精确映射到一个 T1。清单仅保留**相对其 T1 父级会改变编译行为**的类型。领域特定的发散中点(如 pH=7 或 NPS=0)来自 `intrinsicDomain` 或类型内在逻辑,而非专用 T2 类型。 + +| T1 Category | T2 Specific Types | +|---|---| +| `DateTime` | DateTime, Date, Time, Timestamp | +| `DateGranule` | Year, Quarter, Month, Week, Day, Hour, YearMonth, YearQuarter, YearWeek, Decade | +| `Duration` | Duration | +| `Amount` | Amount, Price, Revenue, Cost | +| `Physical` | Quantity, Temperature | +| `Proportion` | Percentage | +| `SignedMeasure` | Profit, PercentageChange, Sentiment, Correlation | +| `GenericMeasure` | Count, Number | +| `Rank` | Rank | +| `Score` | Score, Rating | +| `Index` | Index | +| `GeoCoordinate` | Latitude, Longitude | +| `GeoPlace` | Country, State, City, Region, ZipCode, Address | +| `Entity` | PersonName, Company, Product, Category, Name, String, Unknown | +| `Coded` | Status, Type, Boolean, Direction | +| `Binned` | Range, AgeGroup | +| `ID` | ID | + +**已移除类型**(从 `TYPE_REGISTRY` 和 `SemanticTypes` 中删除;未知字符串回退到 `UNKNOWN_ENTRY`): + +| 已移除 T2 | 改用 | 理由 | +|---|---|---| +| TimeRange | Duration | 相同编译 | +| Distance, Area, Volume, Weight, Speed | Quantity / `Physical` T1 | 单位来自注解 | +| Rate | Percentage | 相同格式 + 聚合 | +| Ratio | Number | 开放域、小数格式 | +| Level | Score | 相同有界/avg 编译 | +| Coordinates | Latitude + Longitude | 配对歧义 | +| Location | Country / State / City | 通用回退 | +| Username, Email, Brand, Department | PersonName / Company / Name | 相同 nominal 编译 | +| Binary, Code | Boolean / Status | 相同 categorical 编译 | +| Bucket | Range | 相同编译 | +| SKU | ID | 相同标识符角色 | + +**T2 相对 T1 的增量:** `Revenue` vs `Price`(可加性 vs 强度型);`Temperature` vs `Quantity`(条件发散);`Month` vs `Year`(cyclic(12) vs open);`Sentiment` vs `Profit` vs `Correlation`(固有 vs 条件发散)。 + +## §2.5 层级作为 DAG + +```text +T0 Family T1 Category T2 Specific +───────── ─────────── ────────────────────── + +Temporal ─────┬── DateTime ──────────── DateTime, Date, Time, Timestamp + ├── DateGranule ───────── Year, Quarter, Month, Week, Day, Hour, + │ YearMonth, YearQuarter, YearWeek, Decade + └── Duration ─────────── Duration + +Measure ──────┬── Amount ────────────── Amount, Price, Revenue, Cost + ├── Physical ─────────── Quantity, Temperature + ├── Proportion ────────── Percentage + ├── SignedMeasure ─────── Profit, PercentageChange, Sentiment, Correlation + └── GenericMeasure ────── Count, Number + +Discrete ─────┬── Rank ─────────────── Rank + ├── Score ────────────── Score, Rating + └── Index ────────────── Index + +Geographic ───┬── GeoCoordinate ────── Latitude, Longitude + └── GeoPlace ─────────── Country, State, City, Region, ZipCode, Address + +Categorical ──┬── Entity ───────────── PersonName, Company, Product, Category, Name, String, Unknown + ├── Coded ────────────── Status, Type, Boolean, Direction + └── Binned ───────────── Range, AgeGroup + +Identifier ───┴── ID ───────────────── ID +``` + +解析沿 T2 → T1 → T0 遍历,应用 progressively finer 规则,并在某层级无特定决策时回退: + +```typescript +function resolveFieldSemantics(annotation, fieldName, values) { + const { semanticType } = normalizeAnnotation(annotation); + const t2 = T2_REGISTRY[semanticType]; + const t1 = t2?.t1 ?? T1_REGISTRY[semanticType]; + const t0 = t1?.t0 ?? T0_REGISTRY[semanticType]; + + // T0: encoding, agg role, zero class (always available) + // T1: format class, agg default, diverging class (if T1 or finer) + // T2: format detail, domain, ticks, interpolation (if T2) + return mergeContext(t0Defaults, t1Refinements, t2Specifics); +} +``` + +## §2.6 循环域类型 + +具有环绕域的类型需要规范排序、不在循环外外推、循环调色板和 radar/polar 提示: + +| Type | Cycle | Values | 可视化关注点 | +|---|---|---|---| +| Month | 12 | Jan–Dec 或 1–12 | 轴不应显示 "13";颜色环绕 | +| Day (weekday) | 7 | Mon–Sun | 同上 | +| Hour | 24 | 0–23 | 圆形图表自然 | +| Direction | 8/16+ | N, NE, E, … | polar/radar 自然 | +| Quarter | 4 | Q1–Q4 | 轴排序 | + +## §2.7 LLM 注解策略 + +| 策略 | 使用的类型 | 适用场景 | LLM 提示规模 | +|---|---|---|---| +| **完整 T2** | 所有具体类型 | 高价值仪表板 | 最大(约 46 种类型) | +| **仅 T1** | 类别级 | 批量注解、成本敏感 | 中等(约 17 种类型) | +| **仅 T0** | 族级 | 快速预览、基于规则回退 | 最小(约 6 种类型) | +| **混合** | 关键字段用 T2,其余用 T1 | 典型交互会话 | 自适应 | + +**混合策略示例** — 图表关键字段用 T2,其余用 T1: + +```json +{ + "revenue": { "semantic_type": "Revenue", "unit": "USD" }, + "month": { "semantic_type": "Month" }, + "product_category": { "semantic_type": "Coded" }, + "customer_name": { "semantic_type": "Entity" }, + "customer_age": { "semantic_type": "GenericMeasure" }, + "region": { "semantic_type": "GeoPlace" }, + "order_date": { "semantic_type": "DateTime" }, + "satisfaction": { "semantic_type": "Score", "intrinsic_domain": [1, 5] } +} +``` + +## §2.8 类型注册表 + +层级控制*哪些*规则在何种粒度触发。每种类型还携带**五个正交维度**,直接驱动可视化属性。这些维度与类型的层级位置一起存在于 `TypeRegistryEntry` 中: + +| 维度 | 取值 | 控制内容 | +|---|---|---| +| **Vis encoding candidates** | `quantitative`, `ordinal`, `nominal`, `temporal`(偏好顺序) | 轴类型、比例尺类型、标记兼容性、排序 | +| **Aggregation role** | `additive`, `intensive`, `signed-additive`, `dimension`, `identifier` | 聚合函数、group-by、仅 tooltip | +| **Domain shape** | `open`, `bounded`, `fixed`, `cyclic` | 域钳制、刻度、外推、polar 提示 | +| **Diverging nature** | `none`, `conditional`, `inherent` | 顺序 vs 发散颜色、中点、图例 | +| **Format class** | `currency`, `percent`, `unit-suffix`, `date`, `time`, `integer`, `plain` | 轴/tooltip 格式、前缀/后缀、精度 | + +**典型类型**(层级位置 + 维度值): + +| Type (T2) | T1 | T0 | Vis encoding | Agg role | Domain | Diverging | Format | +|---|---|---|---|---|---|---|---| +| Month | DateGranule | Temporal | ordinal, temporal | dimension | cyclic (12) | none | date | +| Year | DateGranule | Temporal | temporal, ordinal | dimension | open | none | integer | +| Rating | Score | Discrete | quantitative, ordinal | intensive | bounded [1,N] | conditional | integer | +| Temperature | Physical | Measure | quantitative | intensive | open | conditional | unit-suffix | +| Quantity | Physical | Measure | quantitative | intensive | open, ≥0 | none | unit-suffix | +| Sentiment | SignedMeasure | Measure | quantitative | signed-additive | bounded [-1,1] | inherent | plain | +| Correlation | SignedMeasure | Measure | quantitative | signed-additive | bounded [-1,1] | inherent | plain | +| Profit | SignedMeasure | Measure | quantitative | signed-additive | open | conditional | currency | +| PercentageChange | SignedMeasure | Measure | quantitative | signed-additive | open | conditional | percent | +| Revenue | Amount | Measure | quantitative | additive | open, ≥0 | none | currency | +| Price | Amount | Measure | quantitative | intensive | open, ≥0 | none | currency | +| Percentage | Proportion | Measure | quantitative | intensive | bounded [0,1] or [0,100] | none | percent | +| Count | GenericMeasure | Measure | quantitative | additive | open, ≥0 | none | integer | +| Country | GeoPlace | Geographic | nominal | dimension | open | none | plain | +| Latitude | GeoCoordinate | Geographic | quantitative | dimension | fixed [-90,90] | none | plain | +| Rank | Rank | Discrete | ordinal | dimension | open | none | integer | +| Status | Coded | Categorical | nominal | dimension | fixed | none | plain | +| Direction | Coded | Categorical | nominal | dimension | cyclic (8/16) | none | plain | + +在 T1,构建器继承类别的维度值。在 T2,应用具体覆盖。在 T0,应用保守默认值。下游代码读取解析后的 `FieldSemantics` / `ChannelSemantics`,而非直接读取层级或维度。部分维度值依赖数据,例如根据 distinct-value 数量选择 `Rating` 编码;该消歧在 `resolveFieldSemantics` 中完成,而非注册表中。 + +```typescript +interface TypeRegistryEntry { + t0: T0Family; + t1: T1Category; + visEncodings: VisCategory[]; + aggRole: AggRole; + domainShape: DomainShape; + diverging: DivergingClass; + formatClass: FormatClass; + zeroBaseline: ZeroBaseline; + zeroPad: number; +} +``` + +--- + +# §3 字段注解 + +## §3.1 为何元数据重要 + +裸类型字符串如 `"Rating"` 存在歧义:比例尺是 1–5、1–10 还是 0–100?其他有界或带单位的类型也有类似缺口: + +| Type | 缺失内容 | 为何重要 | +|---|---|---| +| **Rating** / **Score** | 比例尺范围 | 刻度、域、零点决策 | +| **Percentage** | 表示(0–1 vs 0–100) | 格式:`.1%` vs `.1f` + "%" | +| **Temperature** | 单位(°C、°F、K) | 后缀、发散中点(0°C vs 32°F) | +| **Physical measures** | 单位(kg、km、mph) | 格式后缀 | +| **Price / Revenue / Cost** | 货币(USD、EUR) | 格式前缀($、€) | +| **Duration** | 单位(seconds、hours) | 显示策略 | +| **Ordinal categoricals** | 自定义排序 | 非字母顺序(severity、size) | + +开放式度量(`Count`、`Revenue`、`Rank`)和 nominal 字段(`Country`、`Status`)通常无需元数据。 + +## §3.2 SemanticAnnotation + +```typescript +/** + * Enriched semantic annotation for a single field. + * Only `semanticType` is required. Compact form: bare string equals + * `{ semanticType: "..." }`. + */ +interface SemanticAnnotation { + /** Semantic type string (e.g. 'Rating', 'Temperature', 'Price') */ + semanticType: string; + + /** + * Intrinsic domain for bounded/scaled types. + * Drives domainConstraint, exactTicks, zeroBaseline, diverging midpoint. + * NOT for open-ended measures (Revenue, Count, Temperature). + */ + intrinsicDomain?: [number, number]; + + /** + * Unit of measurement — cosmetic when present; omit if mixed units. + * Drives format prefix/suffix and diverging midpoint (°C → 0, °F → 32). + */ + unit?: string; + + /** + * Canonical sort order for domain-specific ordinals. + * Well-known types (Month, DayOfWeek) need not provide this. + */ + sortOrder?: string[]; +} +``` + +## §3.3 哪些类型需要元数据? + +| Type | `intrinsicDomain` | `unit` | `sortOrder` | 原因 | +|---|---|---|---|---| +| **Rating** | 是 — [1,5]、[1,10]、[0,100] | 否 | 否 | 比例尺决定刻度、域、零点 | +| **Score** | 是 — [0,100]、[0,10] | 否 | 否 | 同 Rating | +| **Percentage** | 半 — 从数据推断 | 否 | 否 | 表示影响格式 | +| **Temperature** | 否 | 可选 — °C、°F、K | 否 | 后缀 + 发散提示 | +| **Physical**(任意) | 否 | 可选 | 否 | 仅后缀 | +| **Duration** | 否 | 可选 | 否 | 显示提示 | +| **Price / Revenue / Cost / Amount** | 否 | 可选 — USD、EUR | 否 | 货币前缀 | +| **Latitude / Longitude** | 固定(类型内在) | 否 | 否 | 无需注解 | +| Count, Quantity, Rank, ID, … | 否 | 否 | 否 | 无歧义 | +| **Ordinal categoricals**(Severity、Size) | 否 | 否 | **是** | 领域特定顺序 | +| Well-known ordinals (Month, DayOfWeek) | 否 | 否 | 否 | 内置顺序 | +| Nominal categoricals | 否 | 否 | 否 | 无固有顺序 | +| **Sentiment, Correlation, Profit** | 否 | 可选 currency | 否 | 中点来自类型 | +| **Domain-specific diverging**(pH、NPS) | 是 — 如 [0, 14] | 否 | 否 | 中点来自域 | + +## §3.4 数值表示检测 + +部分类型以不同数值编码出现。构建器在确定字段上下文时解析这些表示: + +| Type | Representations | Detection | +|---|---|---| +| **Percentage** | 0–1 分数 vs 0–100 整数 | `max(data) ≤ 1` → fractional;否则 whole;或 `intrinsicDomain` | +| **Timestamp** | Unix s、Unix ms、ISO string | Magnitude >1e12 → ms;>1e9 → s;string → parse | +| **Month / Day** | Numeric vs abbreviated vs full name | Data type + pattern matching | +| **Boolean** | true/false、0/1、Yes/No | Data type + distinct values | + +**Percentage 影响:** + +| Concern | Fractional (0–1) | Whole-number (0–100) | +|---|---|---| +| Format | `.1%`(d3 ×100) | `.0f` + suffix `%` | +| Domain | [0, 1] | [0, 100] | +| Ticks | 0, 0.25, 0.5, 0.75, 1.0 | 0, 25, 50, 75, 100 | + +优先级:(1)显式 `intrinsicDomain`,(2)数据检查,(3)保守默认。若至少 80% 的绝对值 ≤1,则将字段视为 fractional。 + +## §3.5 接受字符串或对象 + +```typescript +function normalizeAnnotation( + input: string | SemanticAnnotation +): SemanticAnnotation { + if (typeof input === 'string') { + return { semanticType: input }; + } + return input; +} +``` + +图表输入中的 `semantic_types` 接受 `Record`。注解元数据在 `resolveFieldSemantics` 期间流入 `FieldSemantics`:`intrinsicDomain` → 域、刻度、零点和发散中点;`unit` → 格式前缀/后缀;`sortOrder` → `canonicalOrder` 和 ordinal 编码。 + +--- + +# §4 编译流水线 + +## §4.1 四阶段概览 + +| Stage | Function | Input → Output | Concern | +|-------|----------|---------------|---------| +| **1. Field Semantics** | `resolveFieldSemantics()` | Annotation + data → `FieldSemantics` | 这个字段*是什么*? | +| **2. Channel Semantics** | `resolveChannelSemantics()` | FieldSemantics + channel → `ChannelSemantics` | 在此通道上应如何渲染? | +| **3. Layout** | `computeLayout()` | ChannelSemantics + data → `LayoutResult` | 多大?过滤什么? | +| **4. Spec Generation** | `assembleVegaLite()` 等 | ChannelSemantics + template → backend spec | 后端特定输出 | + +`ChannelSemantics` 是**中间表示(IR)**:扁平、目标无关的记录,将上游语义与布局及所有后端(Vega-Lite、ECharts、Chart.js)解耦。 + +```text +┌──────────────────────────────────────────────────────────────────────┐ +│ Stage 1: Field Semantics │ +│ resolveFieldSemantics(annotation, fieldName, values) │ +│ → FieldSemantics (format, agg, domain, ordering) │ +├──────────────────────────────────────────────────────────────────────┤ +│ Stage 2: Channel Semantics │ +│ resolveChannelSemantics(encodings, data, semanticTypes, converted) │ +│ → ChannelSemantics (encoding type, color, ticks, reversal, …) │ +├──────────────────────────────────────────────────────────────────────┤ +│ IR boundary: ChannelSemantics (flat, target-agnostic) │ +├──────────────────────────────────────────────────────────────────────┤ +│ Stage 3: Layout — computeLayout(), filterOverflow() │ +├──────────────────────────────────────────────────────────────────────┤ +│ Stage 4: Spec Generation — assembleVegaLite / ECharts / … │ +│ finalize zero-baseline, template.instantiate, apply layout │ +└──────────────────────────────────────────────────────────────────────┘ +``` + +阶段边界有意收窄:`convertTemporalData()` 在 Stage 2 之前运行一次;`FieldSemantics` 仅内部用于 Stage 2;零点基线最终确定等到 Stage 4,因为需要模板 mark type。 + +## §4.2 字段与通道职责 + +| Decision | Source | Output field | +|---|---|---| +| **来自字段语义(数据身份)** | | | +| Semantic annotation | `resolveFieldSemantics()` | `ChannelSemantics.semanticAnnotation` | +| Number format | `resolveFieldSemantics()` → `resolveFormat()` | `ChannelSemantics.format` | +| Tooltip format | `resolveFieldSemantics()` → `resolveFormat()` | `ChannelSemantics.tooltipFormat` | +| Aggregation default | `resolveFieldSemantics()` → `resolveAggregationDefault()` | `ChannelSemantics.aggregationDefault` | +| Scale type | `resolveFieldSemantics()` → `resolveScaleType()` | `ChannelSemantics.scaleType` | +| Domain constraint | `resolveFieldSemantics()` → `resolveDomainConstraint()` | `ChannelSemantics.domainConstraint` | +| Canonical order | `resolveFieldSemantics()` → `resolveCanonicalOrder()` | 从 `FieldSemantics` 提升 | +| Cyclic ordering | `resolveFieldSemantics()` → `resolveCyclic()` | `ChannelSemantics.cyclic` | +| Sort direction | `resolveFieldSemantics()` → `resolveSortDirection()` | `ChannelSemantics.sortDirection` | +| Zero baseline class | `resolveFieldSemantics()` → `resolveZeroBaseline()` | Stage 4 的内部提示 | +| Binning suggested | `resolveFieldSemantics()` → `resolveBinningSuggested()` | `ChannelSemantics.binningSuggested` | +| **通道特定(可视化)** | | | +| Encoding type (Q/O/N/T) | `resolveEncodingTypeDecision()` | `ChannelSemantics.type` | +| Zero-baseline boolean | `computeZeroDecision()`(Stage 4) | `ChannelSemantics.zero` | +| Color scheme | `getRecommendedColorSchemeWithMidpoint()` | `ChannelSemantics.colorScheme` | +| Temporal format | `resolveTemporalFormat()` | `ChannelSemantics.temporalFormat` | +| Ordinal sort order | `inferOrdinalSortOrder()` | `ChannelSemantics.ordinalSortOrder` | +| Nice rounding | `resolveNice()` | `ChannelSemantics.nice` | +| Tick constraint | `resolveTickConstraint()` | `ChannelSemantics.tickConstraint` | +| Axis reversal | `resolveReversed()` | `ChannelSemantics.reversed` | +| Interpolation | `resolveInterpolation()` | `ChannelSemantics.interpolation` | +| Stackable | `resolveStackable()` | `ChannelSemantics.stackable` | + +## §4.3 resolveFieldSemantics + +```typescript +function resolveFieldSemantics( + annotation: string | SemanticAnnotation, + fieldName: string, + values: any[], +): FieldSemantics; +``` + +内部流程(仅字段内在): + +- `normalizeAnnotation(annotation)` → semantic type + 可选元数据 +- `resolveTiers(semanticType)` → T0/T1 用于规则选择 +- `resolveDefaultVisType(semanticType, values)` → 带数据消歧的编码 +- `resolveFormat(semanticType, unit, fieldName, values)` → `FormatSpec` + tooltip;`detectPrecision(values)` 用于数据驱动小数位 +- `resolveAggregationDefault(semanticType)` → sum / average / undefined +- `resolveZeroBaseline(semanticType, domain)` → meaningful / arbitrary / contextual +- `resolveScaleType(semanticType, values)` → linear / log / sqrt(依赖数据) +- `resolveDomainConstraint(semanticType, domain, values)` → annotation > type-intrinsic > data-inferred +- `resolveCanonicalOrder(semanticType, sortOrder, values)` → annotation 或内置 +- `resolveCyclic(semanticType)` → boolean +- `resolveSortDirection(semanticType)` → ascending / descending +- `resolveBinningSuggested(semanticType, domain, values)` → boolean + +通道级函数(`resolveTickConstraint`、`resolveReversed`、`resolveNice`、color scheme、interpolation、stackable)从同一模块导出,由 Stage 2 调用。 + +## §4.4 resolveChannelSemantics + +```typescript +// Stage 2 entry point → Record +function resolveChannelSemantics(encodings, data, semanticTypes, convertedData?) { + for each channel: + fc = resolveFieldSemantics(normalizeAnnotation(semanticTypes[field]), field, values) + cs = { + field, semanticAnnotation: fc.semanticAnnotation, + type: resolveEncodingType(...), + // promoted from FieldSemantics: + format, tooltipFormat, aggregationDefault, scaleType, + domainConstraint, canonicalOrder, cyclic, sortDirection, binningSuggested, + // channel-resolved: + nice: resolveNice(semanticType, domainShape), + tickConstraint: resolveTickConstraint(semanticType, domain, values), + reversed: resolveReversed(semanticType), + colorScheme: resolveColorScheme(semanticType, annotation, values), + temporalFormat: resolveTemporalFormat(...), + ordinalSortOrder: inferOrdinalSortOrder(...), + interpolation: resolveInterpolation(semanticType), + stackable: resolveStackable(semanticType), + } + return Record +} +``` + +Stage 2 **不**设置 `zero`。Stage 4 调用 `computeZeroDecision()` 并传入 mark type:bar → 为长度完整性包含零;scatter → 数据拟合。 + +## §4.5 FieldSemantics 接口 + +```typescript +/** + * Field-intrinsic properties — semantic type, annotation, and data. + * NOT channel-dependent. Computed in Stage 1. + */ +interface FieldSemantics { + semanticAnnotation: SemanticAnnotation; + defaultVisType: 'quantitative' | 'ordinal' | 'nominal' | 'temporal'; + format: FormatSpec; + tooltipFormat?: FormatSpec; + aggregationDefault?: 'sum' | 'average'; + zeroBaseline: ZeroBaseline | 'unknown'; + scaleType?: 'linear' | 'log' | 'sqrt' | 'symlog'; + domainConstraint?: DomainConstraint; + canonicalOrder?: string[]; + cyclic: boolean; + sortDirection: 'ascending' | 'descending'; + binningSuggested: boolean; +} +``` + +**不在** `FieldSemantics` 上的属性(依赖通道/标记):`nice`、`tickConstraint`、`reversed`、`interpolation`、`stackable`、`colorScheme`、`zero`、`temporalFormat`、`ordinalSortOrder`。 + +## §4.6 ChannelSemantics 接口 + +```typescript +/** Flat IR — sole public interface for layout, templates, and all backends. */ +interface ChannelSemantics { + field: string; + semanticAnnotation: SemanticAnnotation; + type: 'quantitative' | 'nominal' | 'ordinal' | 'temporal'; + format?: FormatSpec; + tooltipFormat?: FormatSpec; + temporalFormat?: string; + aggregationDefault?: 'sum' | 'average'; + zero?: ZeroDecision; // finalized in Stage 4 + scaleType?: 'linear' | 'log' | 'sqrt' | 'symlog'; + nice?: boolean; + domainConstraint?: DomainConstraint; + tickConstraint?: TickConstraint; + ordinalSortOrder?: string[]; + cyclic?: boolean; + reversed?: boolean; + sortDirection?: 'ascending' | 'descending'; + colorScheme?: ColorSchemeRecommendation; + interpolation?: 'linear' | 'step' | 'step-after' | 'monotone'; + binningSuggested?: boolean; + stackable?: 'sum' | 'normalize' | false; +} + +type SemanticResult = Record; +``` + +## §4.7 辅助类型 + +```typescript +interface FormatSpec { + pattern?: string; // d3-format, e.g. "$,.0f", ".1%" + prefix?: string; // "$", "€" + suffix?: string; // "%", "°C", " kg" + decimals?: number; + abbreviate?: boolean; // 1234567 → "1.2M" + temporalPattern?: string; // "%Y", "%b %d" +} + +interface DomainConstraint { + min?: number; + max?: number; + clamp?: boolean; // true = hard clip; false = soft suggestion +} + +interface TickConstraint { + integersOnly?: boolean; + exactTicks?: number[]; // e.g. Rating 1–5 → [1,2,3,4,5] + suggestedCount?: number; + minStep?: number; +} +``` + +## §4.8 布局与规范生成 + +**Stage 3** 仅对 `ChannelSemantics` 和数据操作。关于 stretch 尺寸、overflow 过滤和 facet 网格,请参阅[自动布局算法](/documentation/layout-model)。`declareLayoutMode()` 是模板钩子,通过窄接口让 Stage 4 影响 Stage 3。 + +**Stage 4** 是后端特定的:(1)通过 `computeZeroDecision()` 与 mark type 最终确定零点;(2)翻译 encodings;(3)运行 `template.instantiate()`;(4)应用布局。模板直接读取扁平 `ChannelSemantics`。 + +## §4.9 缓存 + +字段语义可能开销较大,因为包含格式检测和分布分析。按字段缓存,键为 `${fieldName}::${semanticType}::${dataHash}`,其中 `dataHash` 对前约 100 个值做指纹。 + +--- + +# §5 解析规则 + +## §5.1 格式与解析 + +仅当语义上下文增加价值时才覆盖原生格式化:前缀/后缀、缩写、符号或无逗号年份。通用小数(`Number`、`Score`、`Rating`)使用空 `format: {}`,以便 Vega-Lite 自适应精度。提供格式时,`detectPrecision(values)` 将有意义小数位限制在 0–4。 + +| Semantic Type | `pattern` | `prefix` | `suffix` | `abbreviate` | Notes | +|---|---|---|---|---|---| +| **Count** | `,d` | — | — | — | 带千位分隔的整数 | +| **Amount** | data-driven | `$` | — | yes | Tooltip `,.2f` | +| **Price** | `,.2f` | `$` | — | yes | 始终显示分 | +| **Revenue / Cost** | data-driven | `$` | — | yes | Tooltip `,.2f` | +| **Percentage** (0–1) | `.Xp%` | — | — | — | 自动检测表示 | +| **Percentage** (0–100) | data-driven + `d` | — | `%` | — | 无 ×100 | +| **PercentageChange** | `+.X%` 或 `+.Xf` | — | `%` if 0–100 | — | 始终显示符号 | +| **Temperature** | data-driven | — | from unit | — | 单位来自注解 | +| **Score / Rating** | — (empty) | — | — | — | VL 原生轴 | +| **Rank** | `,d` | — | — | — | 整数 | +| **Year** | `d` | — | — | — | 无逗号 | +| **Number** | — (empty) | — | — | — | VL native | +| **Quantity** | data-driven | — | from unit | yes | 单位来自注解 | +| **Profit** | `+` + data-driven | `$` | — | yes | 有符号货币 | +| **Sentiment / Correlation** | `+` + data-driven | — | — | — | 有符号小数 | +| **Latitude / Longitude** | — (empty) | — | — | — | VL native | + +单位/货币优先级为 `annotation.unit` > 列名启发式 > 数据值扫描 > 类型默认。 + +**解析**是编译器职责,由语义类型引导,而非存储在上下文中: + +| Semantic Type | Raw examples | Compiler action | +|---|---|---| +| Amount, Price, Revenue | `"$1,234.56"` | 剥离货币和分隔符 | +| Percentage | `"45.2%"`, `"+12.3%"` | 剥离 `%` 和符号 | +| Temperature, Quantity | `"23.5°C"`, `"75 kg"` | 剥离单位后缀 | +| Duration | `"2h 30m"` | 解析为秒 | +| Timestamp | epoch 或 ISO string | 检测表示 → Date | +| Boolean | `"Yes"`, `0/1` | 规范化为 boolean | +| Month | `"January"`, `1` | 规范形式 | + +## §5.2 聚合默认值 + +| Family | Types | Default | Rationale | +|---|---|---|---| +| Additive measures | Count, Amount, Revenue, Cost, Quantity, Duration | `sum` | 合计 — 求和自然 | +| Intensive measures | Percentage, Temperature, Score, Rating, Price, Correlation, Sentiment | `average` | 比率/状态 — 平均自然 | +| Signed additive | Profit | `sum` | 可为负;求和保留符号 | +| Discrete numeric | Rank, Index, ID | — | 不可聚合 | +| Temporal / Categorical | DateTime, Name, Status, … | — | 不可聚合 | + +自动聚合在多行映射到相同位置编码时注入正确 aggregate:Revenue → sum,Temperature → mean。错误聚合(如对温度求和)会产生荒谬图表。这是显式编译器选项,因为部分上下文会抑制它: + +```typescript +interface CompilerOptions { + /** When true, instantiator injects aggregate transforms for measure fields + * when multiple rows share the same positional encoding (e.g. same X in bar/line). */ + autoAggregate: boolean; +} +``` + +## §5.3 比例尺、域与刻度 + +**比例尺类型**(构建器中依赖数据): + +| Condition | Scale | Example | +|---|---|---| +| Measure + >2 orders of magnitude | `log` | Revenue $1K–$1B | +| Measure + long tail (skew > 2) | `sqrt` | Population | +| Signed + wide range | `symlog` | Profit −$10M to +$500M | +| Percentage (0–100) | `linear` | Completion rate | +| Default quantitative | `linear` | — | + +**域约束** — 有效域 = 内在边界与数据范围的并集。软域从不裁剪合法离群值: + +| Source | Type | Intrinsic | Data | Effective | Clamp | +|---|---|---|---|---|---| +| Annotation | Rating [1,5] | [1,5] | [1,4] | min 1, max 5 | soft | +| Annotation | Score [0,100] | [0,100] | [0,120] | min 0, max 120 | soft | +| Data-inferred | Percentage 0–100 | [0,100] | [0,155] | min 0, max 155 | soft | +| Type-intrinsic | Latitude | [-90,90] | any | [-90,90] | hard | +| Type-intrinsic | Correlation | [-1,1] | any | [-1,1] | hard | + +优先级为 `annotation.intrinsicDomain` > type-intrinsic > data-inferred。小内在跨度(≤20)还会设置 `exactTicks`、`binningSuggested: false`,并细化 `zeroBaseline`。 + +**刻度约束:** + +| Type | `integersOnly` | `exactTicks` | `minStep` | Source | +|---|---|---|---|---| +| Count, Year, Rank, Index | true | — | 1 | Type-intrinsic | +| Rating [1,5] | true | [1,2,3,4,5] | 1 | Annotation | +| Rating [1,10] | true | [1..10] | 1 | Annotation | +| Score [0,100] | true | — (span > 20) | 1 | Annotation | +| Month (1–12) | true | [1..12] | 1 | Type-intrinsic | + +## §5.4 坐标轴与标记 + +| Concern | Rule | +|---|---| +| **Reversed axis** | `Rank` → `true`(第 1 名在顶部);其余 → `false`。模板可覆盖。 | +| **Stacking** | Sum stack:Count、Amount、Revenue、Cost、Quantity、Duration、Profit。Normalize:Percentage。No stack:Temperature、Score、Rating、Rank、Correlation、Sentiment。 | +| **Interpolation** | Rank/Index → `step`;Temperature、Revenue、Profit → `monotone`;default → `linear`。 | +| **Binning** | 建议:Quantity、Amount、Temperature、Percentage、Duration、高基数 Count。不建议:Rating (1–5)、Rank、Year、categorical。 | + +## §5.5 发散与颜色 + +发散处理需要**中点**,按以下优先级解析: + +1. `annotation.unit` 查找(°C → 0,°F → 32) +2. 类型内在中点 +3. `intrinsicDomain` 中点(Rating [1,5] → 3) +4. 数据跨越零 → 中点 0 +5. 数据范围中点(回退) + +| Type | Midpoint | Inherent? | Notes | +|---|---|---|---| +| Temperature | 0 / 32 / 273.15 by unit | conditional | 全为正时用 sequential | +| Profit, PercentageChange | 0 | conditional | 单侧时用 sequential | +| Sentiment, Correlation | 0 | inherent | 中心始终有意义 | +| Score (0–100) | 50 | conditional | 来自域中点 | +| Rating (1–5) | 3 | conditional | 很少发散 | + +**Inherent** 类型始终使用发散调色板,因为正负值有语义含义。**Conditional** 类型仅当数据跨越中点两侧时使用发散调色板;否则使用 sequential 调色板。 + +```typescript +interface ColorSchemeHint { + type: 'categorical' | 'sequential' | 'diverging'; + reversed?: boolean; // Rank: 1 = best = darkest + divergingMidpoint?: number; + inherentlyDiverging?: boolean; +} + +function resolveColorSchemeHint(semanticType, annotation, values): ColorSchemeHint { + const divInfo = resolveDivergingInfo(semanticType, annotation, values); + if (divInfo) { + const spansBoth = min < divInfo.midpoint && max > divInfo.midpoint; + if (divInfo.inherent || spansBoth) { + return { type: 'diverging', divergingMidpoint: divInfo.midpoint, + inherentlyDiverging: divInfo.inherent }; + } + } + return { type: isQuantitative ? 'sequential' : 'categorical' }; +} +``` + +--- + +# §6 示例 + +## §6.1 收入柱状图 + +**Input:** `revenue`,`{ semanticType: "Revenue", unit: "EUR" }`,values ~[124500, 89200, …],channel Y,mark bar。 + +```json +{ + "semanticAnnotation": { "semanticType": "Revenue", "unit": "EUR" }, + "defaultVisType": "quantitative", + "format": { "pattern": "€,.0f", "prefix": "€", "abbreviate": true }, + "tooltipFormat": { "pattern": "€,.2f", "prefix": "€" }, + "aggregationDefault": "sum", + "zeroBaseline": "meaningful", + "scaleType": "linear", + "binningSuggested": true +} +``` + +通道增量:`nice: true`,`stackable: 'sum'`,`interpolation: 'monotone'`,sequential color。Y 轴显示 €0、€100K、…;包含零点基线;tooltip 显示 €124,500.00。 + +## §6.2 温度折线图 + +**Input:** `avg_temp`,`{ semanticType: "Temperature", unit: "°C" }`,values ~[16.8, 31.7, …],channel Y,mark line。 + +```json +{ + "semanticAnnotation": { "semanticType": "Temperature", "unit": "°C" }, + "defaultVisType": "quantitative", + "format": { "pattern": ".1f", "suffix": "°C" }, + "tooltipFormat": { "pattern": ".2f", "suffix": "°C" }, + "aggregationDefault": "average", + "zeroBaseline": "arbitrary", + "binningSuggested": true +} +``` + +通道增量:发散颜色中点 0°C,`interpolation: 'monotone'`,`stackable: false`。轴为数据拟合,不强制 0°C;刻度显示 16°C、20°C、…;线条平滑。 + +## §6.3 排名 bump 图 + +**Input:** `rank`,`Rank`,values [1..10],channel Y,mark line (bump)。 + +```json +{ + "semanticAnnotation": { "semanticType": "Rank" }, + "defaultVisType": "ordinal", + "format": { "pattern": "d" }, + "aggregationDefault": null, + "zeroBaseline": "arbitrary", + "sortDirection": "ascending", + "binningSuggested": false +} +``` + +通道增量:`reversed: true`,`tickConstraint: { integersOnly: true, minStep: 1 }`,`interpolation: 'step'`。Y 轴反转(1 在顶部),刻度为整数,禁用堆叠。 + +## §6.4 带域的 Rating + +**Input:** `rating`,`{ semanticType: "Rating", intrinsicDomain: [1, 5] }`,values [4,3,5,2,4,…],channel Y,mark bar。 + +```json +{ + "semanticAnnotation": { "semanticType": "Rating", "intrinsicDomain": [1, 5] }, + "defaultVisType": "quantitative", + "format": {}, + "tooltipFormat": { "pattern": ".1f" }, + "aggregationDefault": "average", + "zeroBaseline": "arbitrary", + "domainConstraint": { "min": 1, "max": 5, "clamp": false }, + "binningSuggested": false +} +``` + +通道增量:`nice: false`,`tickConstraint: { integersOnly: true, exactTicks: [1,2,3,4,5] }`。域 [1,5] 来自注解;零点为 arbitrary,因为这是 1-based 比例尺;刻度为精确整数;柱状图从零点使用比例长度,因为 Stage 4 对 bar marks 保留 `scale.zero`。 + +--- + +# §7 相关文档 + +- [Architecture](/documentation/architecture) — 完整编译流水线与仓库布局 +- [API reference](/documentation/api-reference) — `ChartAssemblyInput`、encodings、overflow +- [Auto Layout Algorithm](/documentation/layout-model) — Stage 3 尺寸、stretch 与 overflow + +`chart_spec.encodings` 或 `chartProperties` 中的显式覆盖始终优先于编译器默认值。 diff --git a/docs/zh-CN/design-stretch-model.md b/docs/zh-CN/design-stretch-model.md new file mode 100644 index 00000000..6c7e8fc5 --- /dev/null +++ b/docs/zh-CN/design-stretch-model.md @@ -0,0 +1,1096 @@ +# 自动布局算法 + +当数据超出可用画布时,用于自动调整图表坐标轴尺寸的基于物理的模型。 + +初次接触 Flint 尺寸?请从[示例:自动布局](/documentation/chart-sizing)开始,然后回到此处阅读完整算法。 + +**如何阅读本文档:** [§1](#1-layout-mode-classification) 区分 banded 与 continuous 轴并路由到正确模型。[§2](#2-discrete-axis-elastic-budget-model)–[§5](#5-area-layout-2d-pressure-model) 描述四种几何特定模型。[§6](#6-unified-summary) 汇总共享的 pressure–stretch 模式、决策树和实现映射。 + +| § | Model | Geometry | Chart types | +|---|---|---|---| +| [§2](#2-discrete-axis-elastic-budget-model) | Elastic Budget | 1D banded axis | Bar, Histogram, Heatmap, Boxplot | +| [§3](#3-continuous-axis-gas-pressure-model) | Gas Pressure | 2D point cloud | Scatter, Line, Area | +| [§4](#4-circumference-radial-pressure-model) | Circumference | 1D closed loop | Pie, Rose, Sunburst, Radar, Gauge | +| [§5](#5-area-layout-2d-pressure-model) | Area (2D) | 2D filled space | Treemap | + +--- + +## 基准尺寸与 stretch 上限 + +以下每个模型都从**两个数字**开始:图表瞄准的目标尺寸,以及绝不可超过的上限。 + +| Field | Role in the model | Default | +|-------|-------------------|---------| +| `baseSize` | **目标。** 静息长度 $L_0$ / 基准画布 $W_0 \times H_0$,下方所有「pressure = demand ÷ supply」比率均相对此值衡量。 | $400 \times 320$ | +| `canvasSize` | **硬上限。** 任意维度(含 facet 网格)的最大 stretch 尺寸。 | none → $\text{baseSize} \times \text{maxStretch}$(默认 $1.5\times$) | + +stretch 乘数 $\beta$ 限制轴相对基准可增长的距离,按维度设置: + +- **无 `canvasSize`(默认)。** 各维度最多 stretch 到 `maxStretch`(默认 **1.5**),即上限为 $\text{baseSize} \times \text{maxStretch}$。 +- **显式 `canvasSize` 上限。** 上限变为上限与基准之比: + + $$\beta_x = \max\!\left(1, \frac{\text{canvasSize.width}}{\text{baseSize.width}}\right), \qquad \beta_y = \max\!\left(1, \frac{\text{canvasSize.height}}{\text{baseSize.height}}\right).$$ + +**基准永不超过上限。** 计算上限前,基准按维度钳制到上限($L_0 = \min(\text{baseSize}, \text{canvasSize})$)。因此 `canvasSize` *小于*基准时(例如固定槽位且 `baseSize` 保持默认),$L_0$ 会缩小以适配框体。图表压缩并降级文字,而非溢出。省略 `baseSize` 时,单独 `canvasSize` 表现为纯 **fit-to-box**:$L_0 = \text{canvasSize}$,$\beta = 1$,图表无法超出框体。 + +驱动宽度的模型(discrete x-axis、gas-pressure x、treemap x-split)使用 $\beta_x$;驱动高度的模型(discrete y-axis、gas-pressure y、treemap y-split)使用 $\beta_y$;radial/area 上限使用 $\max(\beta_x, \beta_y)$。同一上限约束**分面**布局:small-multiple 网格的总 stretch 范围不可超过 `canvasSize`。下文各节写单一 $\beta$ 时,请按该维度理解为 $\beta_x$ 或 $\beta_y$。 + +--- + +## 交互式演示 + +这四个模型在数据溢出画布时为图表坐标轴定尺寸。拖动控件可观察各模型在添加项、改变 stretch 因子或调整画布大小时的响应。每个演示内均解释控件;下文各节为需要完整推导的读者提供公式。 + +### Elastic budget — discrete (banded) axis · [§2](#2-discrete-axis-elastic-budget-model) + +Bar、Histogram、Heatmap。随类别增加 plot 变宽,然后压缩,stretch 上限耗尽后溢出。 + +```flint-playground +discrete +``` + +### Gas pressure — continuous axis · [§3](#3-continuous-axis-gas-pressure-model) + +Scatter、Line、Area。轴随点密度温和 stretch,而非按项 snap 到 band。 + +```flint-playground +continuous +``` + +### Circumference — radial / closed loop · [§4](#4-circumference-radial-pressure-model) + +Pie、Rose、Radar。随 slice 拥挤,图表增大半径以保持圆周可读。 + +```flint-playground +circumference +``` + +### Area — 2D filled space · [§5](#5-area-layout-2d-pressure-model) + +Treemap。画布面积增长,使每个矩形保持足够大以便阅读,而非缩成细条。 + +```flint-playground +area +``` + +--- + +## 目录 + +- [§1 布局模式分类](#1-layout-mode-classification) + - [§1.1 Banded vs Non-Banded](#11-banded-vs-non-banded) + - [§1.2 决策树](#12-decision-tree) + - [§1.3 Vega-Lite 实现说明](#13-vega-lite-implementation-notes) +- [§2 离散轴(Elastic Budget Model)](#2-discrete-axis-elastic-budget-model) + - [§2.1 问题](#21-problem) + - [§2.2 参数](#22-parameters) + - [§2.3 三种状态](#23-three-regimes) + - [§2.4 幂律 Elastic Budget](#24-power-law-elastic-budget) + - [§2.5 理论基础](#25-theoretical-foundation-spring-model) + - [§2.6 分组项](#26-grouped-items) + - [§2.7 各 Mark 类型指南](#27-per-mark-type-guidelines) + - [§2.8 分面图表](#28-faceted-charts) + - [§2.9 摘要](#29-summary) +- [§3 连续轴(Gas Pressure Model)](#3-continuous-axis-gas-pressure-model) + - [§3.1 问题](#31-problem) + - [§3.2 参数](#32-parameters) + - [§3.3 各轴 Stretch](#33-per-axis-stretch) + - [§3.4 Positional ≥ Series 约束](#34-positional--series-constraint) + - [§3.5 参数表](#35-parameter-table) + - [§3.6 算例](#36-worked-examples) + - [§3.7 摘要](#37-summary) + - [§3.8 分面连续布局](#38-faceted-continuous-layout-per-subplot-baseline--pressure--ar-blend--fit) + - [§3.8.1 问题](#381-problem) + - [§3.8.2 各子图基准](#382-per-subplot-baseline-canvas) + - [§3.8.3 Banking AR](#383-banking-ar-multi-scale-slope-optimization) + - [§3.8.4 Gas–Banking 混合](#384-gasbanking-ar-blend) + - [§3.8.5 面积预算](#385-area-budget-and-shape) + - [§3.8.6 拟合预算](#386-fit-to-budget-preserving-ar) + - [§3.9 Band AR 混合](#39-band-ar-blending) +- [§4 圆周(Radial Pressure Model)](#4-circumference-radial-pressure-model) + - [§4.1 问题](#41-problem) + - [§4.2 参数](#42-parameters) + - [§4.3 有效项数](#43-effective-item-count) + - [§4.4 Pressure 与 Stretch](#44-pressure-and-stretch) + - [§4.5 画布尺寸](#45-canvas-sizing) + - [§4.6 Gauge 分面](#46-gauge-faceting) + - [§4.7 参数表](#47-parameter-table) + - [§4.8 摘要](#48-summary) +- [§5 面积布局(2D Pressure Model)](#5-area-layout-2d-pressure-model) + - [§5.1 问题](#51-problem) + - [§5.2 参数](#52-parameters) + - [§5.3 有效项数](#53-effective-item-count) + - [§5.4 Pressure 与偏置分割](#54-pressure-and-biased-split) + - [§5.5 算例](#55-worked-examples) + - [§5.6 摘要](#56-summary) +- [§6 统一摘要](#6-unified-summary) + +--- + +# §1 布局模式分类 + +## §1.1 Banded vs Non-Banded + +布局模型需决定**如何**为每个位置轴分配空间。两个独立属性驱动该决策: + +1. **Scale type** — 字段的 Vega-Lite 编码类型。 +2. **Mark geometry** — 标记是否占据固定宽度 band 或点状位置。 + +### Banded layout + +**Banded** 轴为每个数据位置分配固定宽度槽位(band)。布局模型控制每槽的步长。项通过 band 的宽度/面积阅读。 + +| Condition | Example | +|---|---| +| **Discrete scale**(nominal / ordinal) | Bar 图上的类别、ordinal 月份 | +| **Continuous scale + band mark** | X 为 quantitative 或 temporal 的 Bar 图(年份为数字) | +| **Binned axis**(`bin: true`) | Histogram 分箱 — 无论 scale 如何,每 bin 是一个 band | + +### Non-banded layout + +**Non-banded** 轴在连续范围内按数据决定的位置放置项。布局模型控制整体画布尺寸,但**不**分配 per-item 槽位。 + +| Condition | Example | +|---|---| +| **Continuous scale + point mark** | Scatter、Line、Area | + +### 摘要矩阵 + +| | Band mark (bar, rect, boxplot) | Point mark (circle, line, area) | +|---|---|---| +| **Discrete scale** (N/O) | Banded — §2 | Banded — §2 (*) | +| **Continuous scale** (Q/T) | Banded — §2 | Non-banded — §3 | + +(*) Discrete scale 无论 mark type 均为 banded — VL 为每个类别分配 band。 + +## §1.2 决策树 + +``` +For each positional axis (x, y): + +1. Is the VL encoding type nominal or ordinal? + → YES: Banded (discrete). Use §2 directly. + +2. Is the axis binned (enc.bin = true)? + → YES: Banded (continuous). Use §2 with bin count as N. + +3. Does the template declare this axis as banded? + (axisFlags.banded = true, e.g. bar/rect/boxplot marks) + → YES: Banded (continuous). Use §2 with field cardinality as N. + +4. Otherwise: + → Non-banded (continuous). Use §3. +``` + +> **Implementation:** 决策在 `compute-layout.ts` 中通过 `axisFlags.x.banded` / `axisFlags.y.banded` 和 `isDiscreteType()` 检查完成。见 `computeLayout()` 约 155–230 行。 + +## §1.3 Vega-Lite 实现说明 + +§2 elastic budget 模型适用于 discrete-banded 和 continuous-banded 轴,但 **Vega-Lite 实现因 scale type 而异**: + +### Discrete banded (nominal / ordinal) + +VL 原生支持基于 step 的尺寸: + +```json +{ "width": { "step": ℓ } } +``` + +VL 创建 band scale,为每个类别分配 $\ell$ 像素,并将图表尺寸设为 $N \times \ell$。 + +分组 bar(xOffset / yOffset): + +```json +{ "width": { "step": ℓ_group, "for": "position" } } +``` + +### Continuous banded (quantitative / temporal + band mark) + +VL **不**支持 continuous scale 上的 `{ "step": N }`,因此 Flint 分两阶段处理: + +**Phase 1 — Canvas sizing (assemble.ts):** + +``` +continuousWidth = stepSize × (N + 1) +``` + +`+1` 在两侧各加 half-step padding。scale domain 扩展 ±halfStep,使位置对齐如同 discrete band scale。 + +**Phase 2 — Mark sizing (postProcessing):** + +由于 VL 不在 continuous scale 上自动定 bar 尺寸: +1. 排序唯一字段值;找 `minGap`(最小连续差)。 +2. 转为像素:`pixelsPerUnit = subplotDim × (N−1) / (dataRange × N)`。 +3. `markSize = min(stepSize × 0.9, floor(minGap × pixelsPerUnit))`。 +4. 通过 `{ "mark": { "size": markSize } }` 应用(rect 用 `width`/`height`,fill ratio 0.98)。 + +### 对比 + +| Aspect | Discrete banded | Continuous banded | +|---|---|---| +| VL scale type | `nominal` / `ordinal` (band scale) | `quantitative` / `temporal` (linear/time scale) | +| Step control | width/height 上的 `{ "step": ℓ }` | 手动:`config.view.continuousWidth = ℓ × (N+1)` | +| Mark sizing | 自动(VL 填充 band) | 手动:`mark.size` 来自 min-gap 计算 | +| Domain padding | 自动(band scale) | 手动:domain 扩展 ±halfStep | +| Sort control | `encoding.sort` | 数据决定(continuous scale) | + +### 何时优先 continuous banded + +- 数据具有**自然顺序和算术含义**(年份、日期、价格)。 +- 数据具有**不规则间距** — continuous scale 保留比例位置。 +- 模板声明 `axisFlags.banded = true` 同时保持 VL 编码类型为 Q/T。 + +`templates/utils.ts` 中的 `detectBandedAxis` 函数处理该决策。 + +--- + +# §2 离散轴(Elastic Budget Model) + +## §2.1 问题 + +离散轴沿长度为 $L_0$ 像素的 1D 段显示 $N$ 个 banded 项(类别、分箱、组)。每项理想占据 $\ell_0$ 像素(自然长度)。当 $N \cdot \ell_0 > L_0$ 时项溢出。 + +需平衡两个竞争目标: + +1. **项抵抗压缩** — 每项向外推以维持 $\ell_0$,且不能低于 $\ell_{\min}$。 +2. **轴抵抗扩展** — 轴可超出 $L_0$ stretch,但有硬上限 $L_{\max}$。 + +## §2.2 参数 + +| Symbol | Meaning | Code mapping | Default | +|---|---|---|---| +| $L_0$ | Natural axis length | `width` / `height` (base size) | 400 px | +| $L_{\max}$ | Maximum axis length | `base × β`(β 来自 `maxStretch` 或 `canvasSize`) | 800 px | +| $N$ | Number of banded items | Field cardinality | data-dependent | +| $\ell_0$ | Natural length per item | `defaultStepSize` | ~20 px | +| $\ell_{\min}$ | Minimum length per item | `minStep` option | 6 px | +| $\alpha$ | Elasticity exponent | `elasticity` option | 0.5 | +| $\beta$ | Maximum stretch multiplier | `maxStretch`,或从 `canvasSize` 推导 | 1.5 | + +> **Code defaults:** 未设置 `canvasSize` 上限时,`elasticity: 0.5`、`minStep: 6`、`maxStretch: 1.5`。`defaultStepSize` 根据画布尺寸动态计算:`round(20 × max(1, sizeRatio) × defaultStepMultiplier)`。 + +## §2.3 三种状态 + +### Regime 1: No compression needed + +**Condition:** $N \cdot \ell_0 \leq L_0$ + +所有项以自然长度容纳: + +$$\ell = \ell_0, \quad L = N \cdot \ell_0$$ + +### Regime 2: Overflow beyond recovery + +**Condition:** $N \cdot \ell_{\min} \geq L_{\max}$ + +即使最小项长和最大 stretch 仍无法容纳所有项。多余项被截断: + +$$N' = \left\lfloor \frac{L_{\max}}{\ell_{\min}} \right\rfloor, \quad \ell = \ell_{\min}, \quad L = L_{\max}$$ + +### Regime 3: Elastic equilibrium + +**Condition:** $N \cdot \ell_0 > L_0$ 且 $N \cdot \ell_{\min} < L_{\max}$ + +项溢出但可通过压缩项和/或 stretch 轴容纳。此处应用 elastic 模型。 + +## §2.4 幂律 Elastic Budget + +这是**已实现模型**。轴使用 pressure 比率的幂律 stretch: + +**Pressure:** + +$$p = \frac{N \cdot \ell_0}{L_0}$$ + +**Stretch factor:** + +$$s = \min(\beta,\; p^{\alpha})$$ + +**Resulting step size:** + +$$\ell = \frac{L_0 \cdot s}{N} = \frac{L_0 \cdot p^{\alpha}}{N}$$ + +$\alpha = 0.5$ 时,溢出加倍仅使 stretch 增加 $\sqrt{2} \approx 1.41\times$ — 自然渐进响应。 + +**Clamping:** step 钳制到 $[\ell_{\min},\; \ell_0]$,轴长钳制到 $[L_0,\; L_{\max}]$。 + +> **Implementation:** `core/decisions.ts` 中的 `computeElasticBudget()`(约 549–569 行)。由 `computeAxisStep()` 调用,处理 nominal 和 continuous-as-discrete 情况。 + +## §2.5 理论基础(spring model) + +幂律模型可由物理类比动机:$N$ 个相同弹簧装在盒内。 + +**Setup:** +- 每个弹簧(项)自然长度 $\ell_0$,固体长度 $\ell_{\min}$,弹簧常数 $k_1$。 +- 盒子(轴)自然长度 $L_0$,最大长度 $L_{\max}$,弹簧常数 $k_2$。 + +**Force balance at equilibrium:** + +$$N \cdot k_1 \cdot (\ell_0 - \ell) = k_2 \cdot (N \cdot \ell - L_0)$$ + +**Equilibrium step size**(使用刚度比 $\kappa = k_1 / k_2$): + +$$\boxed{\ell = \frac{\kappa \cdot \ell_0 + L_0 / N}{1 + \kappa}}$$ + +**$\kappa$ 的解释:** +- $\kappa \to \infty$:项不压缩;墙吸收一切($\ell \to \ell_0$)。 +- $\kappa \to 0$:项压缩以适配固定轴($\ell \to L_0 / N$)。 +- $\kappa = 1$:压缩均分($\ell = (\ell_0 + L_0/N) / 2$)。 + +线性 spring 模型在物理上更直观,可独立调节项与墙的刚度($\kappa$)。此处作为幂律模型的理论动机呈现。 + +**Nonlinear (progressive-rate) variant:** 将线性弹簧换为硬化弹簧 $F_1(\ell) = k_1 \cdot ((\ell_0 - \ell) / (\ell_0 - \ell_{\min}))^{\gamma}$ 直接导向实现中的幂律形式。 + +**与幂律实现的映射:** + +| Linear spring model | Power-law implementation | +|---|---| +| $\kappa$(刚度比 $k_1/k_2$) | $\alpha$(elasticity exponent) | +| $\ell = (\kappa \cdot \ell_0 + L_0/N) / (1 + \kappa)$ | $s = \min(\beta, p^{\alpha})$;$\ell = L_0 \cdot s / N$ | +| 在 $\ell_0$ 与 $L_0/N$ 间均匀插值 | 偏向 $\ell_0$ 的幂曲线插值 | +| 两参数($k_1$、$k_2$) | 一参数($\alpha$) | +| 物理上更直观 | 更紧凑;自然渐进 | + +## §2.6 分组项 + +分组项(如每组 $m$ 个子 bar 的分组 bar 图)作为特例:**组**是压缩单位,而非单个项。 + +| Parameter | Simple discrete | Grouped bar ($m$ sub-bars) | +|---|---|---| +| $\ell_0$ (natural) | `defaultStepSize` | $m \times$ `defaultStepSize` | +| $\ell_{\min}$ (solid) | `minStep` (6 px) | $2m$ px(每子 bar 2 px) | +| $N$ (item count) | Field cardinality | **组**数量 | + +elastic budget 公式不变 — 仅参数值变化。 + +**Example:** 400 px 轴上 15 组 × 3 子 bar: +- $N = 15$,$\ell_0 = 60$,ideal $= 900 > 400$ → Regime 3。 +- $\alpha = 0.5$:$p = 900/400 = 2.25$,$s = \min(1.5, 2.25^{0.5}) = 1.50$。 +- Budget $= 400 \times 1.5 = 600$,step $= 600/15 = 40$ px per group。 + +> **Implementation:** `computeLayout()` 中通过 `group` 通道检测分组。`xHasGrouping` 为 true 时,step 按组计算,`xStepUnit = 'group'`,并强制最小组间距 3 px。 + +## §2.7 各 Mark 类型指南 + +不同 mark type 有不同视觉占用和压缩容忍度。模板可通过 `defaultStepMultiplier` 和 `overrideDefaultSettings` 调节行为。 + +**设计指南**(300 px 参考画布,`defaultStepSize` ≈ 20 px): + +| Mark type | $\ell_0$ | $\ell_{\min}$ | Compression tolerance | Rationale | +|---|---|---|---|---| +| **Bar** | 20 px | 6 px | Moderate | 宽度编码项,不能缩太多 | +| **Stacked bar** | 20 px | 6 px | Low | 堆叠段过薄不可读 | +| **Grouped bar** ($m$) | $20m$ px | $2m$ px | Low | 失去子 bar 区分代价高 | +| **Lollipop** | 14 px | 4 px | High | 点(位置)承载编码,非宽度 | +| **Heatmap / rect** | 20 px | 8 px | Very low | 颜色单元需面积才可感知 | +| **Boxplot** | 24 px | 10 px | Low | 内部结构(box/whiskers/median)早失 | +| **Strip / jitter** | 24 px | 6 px | Moderate | 过窄时点坍成线 | +| **Histogram** | 16 px | 4 px | High | 分布形状抗压缩好 | +| **Candlestick** | 18 px | 8 px | Low | open/close 实体 + 影线需空间 | + +**设计原则:** +1. 以**宽度/面积**编码值的 mark(bar、rect)→ 更高 $\ell_0$,更低压缩容忍。 +2. 以**位置**编码值的 mark(lollipop、bump)→ 更高压缩容忍。 +3. 有**内部结构**的 mark(boxplot、candlestick)→ 更高 $\ell_{\min}$。 +4. 展示**分布形状**的 mark(histogram)→ 可更窄。 + +> **Note:** 模板目前主要通过 `defaultStepMultiplier`(按比例缩放 $\ell_0$)和 `overrideDefaultSettings` 调节布局。per-mark-type spring 刚度($\kappa$)是设计愿景,代码中尚未单独参数化。 + +## §2.8 分面图表 + +分面将一张图拆成子图网格。画布需容纳 $F$ 个面板,各有轴尺寸。本节涵盖**网格布局**:stretch、子图尺寸、换行。分面内 continuous 轴 AR 混合见 [§3.8](#38-faceted-continuous-layout-per-subplot-baseline--pressure--ar-blend--fit)。 + +### §2.8.1 Facet stretch factor + +总画布 stretch 以容纳分面: + +$$\lambda_f = \min(\beta,\; F^{\alpha_f})$$ + +其中 $\alpha_f$ = `facetElasticity`(默认 0.3),$\beta$ = `maxStretch`(无 `canvasSize` 上限时默认 1.5)。 + +分面 stretch 使用**更温和指数**($\alpha_f = 0.3$ vs discrete 项的 $\alpha = 0.5$),因为每个子图是自包含图表 — 小子图仍可读,而 3 px bar 不行。 + +> **Implementation:** `computeLayout()` 使用 `facetElasticityVal = 0.3` 和 `resolveStretchCaps()` 的 per-dimension 上限。 + +### §2.8.2 Subplot sizing + +每个子图分得 stretch 画布的一份: + +$$W_{\text{sub}} = \max\!\left(S_{\min},\; \frac{W_0 \cdot \lambda_f - \text{fixedPad}}{F_c} - \text{gap}\right)$$ + +| Symbol | Meaning | Default | +|---|---|---| +| $F_c, F_r$ | Facet columns / rows | data-dependent | +| $\alpha_f$ | Facet elasticity | 0.3 | +| $S_{\min}$ | Minimum subplot size (continuous axis) | 60 px | + +### §2.8.3 Facet-mode shrink limits + +分面下轴可比单图模式**进一步**缩小,因为读者比较面板间模式而非精确读单个值。 + +| Mark type | $\ell_{\min}^{f}$ (banded) | $S_{\min}$ (continuous) | +|---|---|---| +| Bar / stacked bar | 3 px | 60 px | +| Heatmap / rect | 4 px | 40 px | +| Boxplot | 6 px | 60 px | +| Line / area | — | 40 px | +| Ridge / density | — | 20 px | +| Scatter | — | 60 px | + +### §2.8.4 Faceted discrete axis + +spring 模型**按子图**运行:$W_{\text{sub}}$ 成为 $L_0$,$N_{\text{items}}$ 为每面板计数。若仍溢出,截断到 $N' = \lfloor W_{\text{sub}} / \ell_{\min} \rfloor$。 + +### §2.8.5 Faceted continuous axis + +gas pressure 模型(§3)在每个子图内运行,容器为 $W_{\text{sub}} \times H_{\text{sub}}$。子图尺寸在各面板间统一以保持视觉一致。 + +### §2.8.6 Facet wrap (column-only folding) + +仅指定 column facet 且 $F$ 超过可容纳最大列数时,面板换行成 2D 网格: + +1. **Maximum columns:** $F_{c,\max} = \lfloor \text{effectiveW} / (S_{\min} + \text{gap}) \rfloor$,其中 $\text{effectiveW} = W_0 \times \beta - \text{fixPad}$。 +2. **Single row:** 若 $F \leq F_{c,\max}$,所有面板一行,无需换行。 +3. **Wrapping:** 否则从 $F_c = F_{c,\max}$ 列开始,$F_r = \lceil F / F_c \rceil$ 行。 +4. **Widow avoidance:** 若最后一行恰好 1 个面板("widow"),$F_c$ 减 1 重算。在 $F_c > 2$ 且仍有 widow 时重复。更均匀重分配:11 面板 maxCols=5 会得 5×3 带 1 孤儿,算法改试 4×3,末行 3 面板。 + +最小子图尺寸($S_{\min}$)按轴感知: +- **Discrete/banded axes:** $S_{\min} = \ell_{\min} \times N$(minStep × 每轴值计数)。 +- **Continuous axes:** $S_{\min} = \text{baseMinSubplot}$(默认 60 px),两轴均为 continuous 时由 banking AR 调整 — 较短维保持基准,较长维最多 $\beta \times$ 基准。确保 line chart(landscape AR)获得更宽最小子图,产生更少更宽面板。 + +> **Implementation:** `compute-layout.ts` 中的 `computeFacetGrid()`。在 `computeLayout()` **之前**运行,以打破换行与轴尺寸间的循环依赖。 + +## §2.9 摘要 + +| Symbol | Meaning | Default | +|---|---|---| +| $N$ | Number of discrete items | data-dependent | +| $\ell_0$ | Natural step size | ~20 px | +| $\ell_{\min}$ | Minimum step size | 6 px | +| $\alpha$ | Elasticity exponent | 0.5 | +| $\beta$ | Maximum stretch | 1.5 | + +``` +Given: N items, natural length ℓ₀, solid length ℓ_min, + axis rest length L₀, maxStretch β, elasticity α + +pressure = N · ℓ₀ / L₀ + +if pressure ≤ 1: + ℓ = ℓ₀ # Regime 1: fits + +elif N · ℓ_min ≥ β · L₀: + ℓ = ℓ_min, truncate to N' items # Regime 2: overflow + +else: + stretch = min(β, pressure^α) # Regime 3: elastic + ℓ = L₀ · stretch / N + ℓ = clamp(ℓ, ℓ_min, ℓ₀) +``` + +> **Key functions:** `core/decisions.ts` 中的 `computeElasticBudget()`、`computeAxisStep()`;`core/compute-layout.ts` 中的 `computeLayout()`。 + +--- + +# §3 连续轴(Gas Pressure Model) + +## §3.1 问题 + +连续轴在 2D 画布上显示 $N$ 个点状项(scatter 点或 line 顶点)。与离散项不同,这些 mark 不占据固定 band;它们位于数据决定的位置。每个 mark 有视觉截面积 $\sigma$(px²)。 + +**为何不用 spring:** 连续 mark 不拥有槽位。100 点与 10 点的 scatter 可共用同一画布;差异是**密度**,非 per-item 分配。因此 pressure 模型比 per-item spring 更合适。 + +## §3.2 参数 + +| Symbol | Meaning | Code mapping | Default | +|---|---|---|---| +| $W_0, H_0$ | Natural canvas dimensions | `subplotWidth`, `subplotHeight` | 400 × 320 px | +| $\sigma$ | Mark cross-section (px²) | `markCrossSection` | 30 px² | +| $\sigma_x, \sigma_y$ | Per-axis cross-sections | `markCrossSectionX/Y` | chart-type specific | +| $\alpha_c$ | Elasticity exponent | `elasticity` | 0.3 | +| $\beta_c$ | Maximum stretch | `maxStretch` | 1.5 | + +> **Code defaults:** `core/decisions.ts` 中的 `DEFAULT_GAS_PRESSURE_PARAMS` — `markCrossSection: 30`、`elasticity: 0.3`、`maxStretch: 1.5`。 + +**为何 $\beta_c$ 小于 discrete $\beta$:** 连续轴以**沿比例尺的位置**编码 — 感知上最稳健的通道(Cleveland & McGill, 1984)。scatter 压缩后仍可读,因为相对位置保留。离散轴以 **band 的长度/面积**编码,退化更快。 + +| | Discrete axis | Continuous axis | +|---|---|---| +| Primary encoding | Length / area of band | Position along scale | +| Recommended $\beta$ | 2.0 | 1.5 | + +## §3.3 各轴 Stretch + +拥挤几乎总是不对称。Line chart 上 X 由时间点驱动,Y 由重叠 series 驱动。各轴独立 stretch。 + +### Mode 1: Positional (default) + +沿轴统计唯一像素位置(约 1 px 分辨率分桶)。每位置需 $\sigma_{1d} = \sqrt{\sigma}$ 像素: + +$$p_{1d} = \frac{\text{uniquePos} \cdot \sigma_{1d}}{\text{dim}_0}$$ + +$$ +s = \begin{cases} +1 & \text{if } p_{1d} \leq 1 \cr +\min(\beta_c,\; p_{1d}^{\,\alpha_c}) & \text{if } p_{1d} > 1 +\end{cases} +$$ + +### Mode 2: Series-count (`seriesCountAxis`) + +当 `seriesCountAxis` 设为(`'x'`、`'y'` 或 `'auto'`)时,指定轴用 distinct series(color ∪ detail 字段)数量计算 pressure。`'auto'` 解析为: +- 2D path(两轴 continuous):Y 轴。 +- 1D path(一 continuous + 一 discrete):continuous 轴。 + +$$p_{\text{series}} = \frac{n_{\text{series}} \cdot \sigma}{\text{dim}_0}$$ + +此处 $\sigma$ **直接**使用(非开方),因为 series 计数本质 1D。 + +> **Implementation:** `core/decisions.ts` 中的 `computeGasPressure()`(约 442–508 行)。2D path(两轴 continuous)和 1D path 在 `computeLayout()` 约 275–425 行分别处理。 + +## §3.4 Positional ≥ Series 约束 + +两轴均为 continuous 的图表(line、area)中,更多 series 也意味着 **positional** 轴视觉更乱 — 更多重叠线意味着更多交叉和平行笔画争夺读者注意力。positional 轴理想 stretch 至少提升到 series 轴理想 stretch: + +$$\text{ideal}_{\text{positional}} = \max(\text{ideal}_{\text{positional}},\; \text{ideal}_{\text{series}})$$ + +设置 `maintainContinuousAxisRatio` 时,两轴使用两者 stretch 的最大值。 + +## §3.5 参数表 + +| Chart type | $\sigma_x$ | $\sigma_y$ | $\alpha_c$ | $\beta_c$ | seriesCountAxis | +|---|---|---|---|---|---| +| Scatter | 30 | 30 | 0.3 | 1.5 | — | +| Line | 100 | 20 | 0.3 | 1.5 | auto (→ Y) | +| Dotted Line | 100 | 20 | 0.3 | 1.5 | auto (→ Y) | +| Area | 100 | 20 | 0.3 | 1.5 | auto (→ Y) | +| Streamgraph | 100 | 20 | 0.3 | 1.5 | auto (→ Y) | +| Bump | 80 | 20 | 0.3 | 1.5 | auto (→ Y) | +| Stacked Bar | 20 | 20 | 0.3 | 1.5 | auto (→ Y*) | + +\* Stacked bar 的 X 为 discrete(§2),Y 为 continuous。`auto` 经 1D path 解析到 Y。 + +## §3.6 算例 + +### Series-axis stretch ($\sigma = 20$, $\text{dim}_0 = 300$, $\alpha_c = 0.3$, $\beta_c = 1.5$) + +| Scenario | nSeries | pressure | stretch | Final dim | +|---|---|---|---|---| +| 8 series (typical) | 8 | 0.53 | 1.0 | 300 | +| 15 series (moderate) | 15 | 1.0 | 1.0 | 300 | +| 20 series (busy) | 20 | 1.33 | 1.09 | 328 | +| 40 series (extreme) | 40 | 2.67 | 1.35 | 406 | + +### Combined positional + series (positional ≥ series constraint) + +| Scenario | nDates | nSeries | raw X | raw Y | final X | final Y | +|---|---|---|---|---|---|---| +| 12 dates × 20 series | 12 | 20 | 1.0 | 1.09 | **1.09** | 1.09 | +| 100 dates × 40 series | 100 | 40 | 1.32 | 1.35 | **1.35** | 1.35 | +| 100 dates × 60 series | 100 | 60 | 1.32 | 1.50 | **1.50** | 1.50 | +| 200 dates × 3 series | 200 | 3 | 1.50 | 1.0 | 1.50 | 1.0 | +| 200 dates × 20 series | 200 | 20 | 1.50 | 1.09 | 1.50 | 1.09 | + +## §3.7 摘要 + +| Symbol | Meaning | Default | +|---|---|---| +| $\sigma$ | 2D mark cross-section (px²) | 30 | +| $\sigma_{1d}$ | 1D projection: $\sqrt{\sigma}$ | ~5.5 | +| $\alpha_c$ | Elasticity exponent | 0.3 | +| $\beta_c$ | Max stretch | 1.5 | + +``` +Given: data points with x/y values, per-axis cross-sections σ_x σ_y, + canvas W₀×H₀, elasticity αc, maxStretch βc, + optional seriesCountAxis + +For each axis (X, Y): + if seriesCountAxis resolves to this axis: + nSeries = |distinct color ∪ detail values| + pressure = nSeries · σ / dim₀ + else: + uniquePos = |{ round(v · px_per_unit) : v ∈ data }| + σ_1d = √σ + pressure = uniquePos · σ_1d / dim₀ + + if pressure ≤ 1: + stretch = 1 + else: + stretch = min(βc, pressure^αc) + +// Positional ≥ Series constraint (when seriesCountAxis is set): +stretch_positional = max(stretch_positional, stretch_series) + +W = W₀ · stretch_x +H = H₀ · stretch_y +``` + +> **Key functions:** `core/decisions.ts` 中的 `computeGasPressure()`;`core/compute-layout.ts` 中 gas-pressure 集成。 + +## §3.8 分面连续布局(Per-Subplot Baseline → Pressure → AR Blend → Fit) + +### §3.8.1 问题 + +图表分面时,gas pressure 模型首先要回答:**每个子图的数据相对哪块画布拥挤?** + +朴素做法:对全画布运行 gas pressure,再除以列/行数。高估可用空间,因为每个子图只占画布一部分。子图过大,超出总预算。 + +另一朴素做法:先把原始画布除以列/行数,再 per subplot 运行 gas pressure。低估可用空间,因为忽略布局引擎将应用的 facet stretch。gas pressure 看到人为微小画布并立即饱和。 + +正确答案是:**gas pressure 在应用 facet elasticity 后的 per-subplot 画布上运行**。与 discrete 轴使用相同 stretch 公式。 + +### §3.8.2 Per-Subplot Baseline Canvas + +gas pressure 运行前,计算仅靠 facet stretch 每个子图会得到什么: + +$$W_{\text{sub}} = \max\!\left(S_{\min},\; \frac{W_0 \cdot \lambda_f - \text{fixPad}}{F_c} - \text{gap}\right)$$ + +其中 $\lambda_f = \min(\beta,\; F_c^{\,\alpha_f})$ 为 facet elasticity stretch(§2.8.1)。单面板图($F_c = 1$)时 $W_{\text{sub}} = W_0$。 + +这给 gas pressure 现实基准:任何额外 gas-pressure stretch 之前子图将占据的空间。 + +> **Implementation:** `computeLayout()` 中的 `perSubplotCanvasW/H`(约 410–420 行)。使用 `facetElasticityVal = 0.3` 和 `resolveStretchCaps()` 的 per-dimension 上限。 + +### §3.8.3 Banking AR (Multi-Scale Slope Optimization) + +有连接 mark 的图表(line、area、streamgraph),数据有由线段斜率决定的**感知最优宽高比**。这是 *banking to 45°* 原则(Cleveland, 1993):图表应塑形使中位线段斜率接近 45°,趋势最可见。 + +我们使用**多尺度 banking**(Heer & Agrawala, 2006),考虑多平滑级别的斜率: + +**Algorithm:** + +1. **按 series 分组**(color ∪ detail 字段)。每 series 按 X 排序。 +2. **对每个 scale** $k = 0, 1, 2, \ldots$(窗口大小 $= 2^k$): + - 用宽度 $2^k$ 的非重叠 box filter 平滑每 series。 + - 计算连续平滑点间绝对斜率:$|s| = |\Delta y / \Delta x|$(归一化数据坐标)。 + - 取该 scale 的**中位**绝对斜率。 +3. **合并** per-scale 中位数为几何平均: + $$\text{combinedSlope} = \exp\!\left(\frac{1}{K}\sum_{k=0}^{K}\ln(\text{median}_k)\right)$$ +4. **钳制**到 $[0.5,\; 3.0]$。 +5. **Landscape floor**(仅 connected marks):$\text{AR} = \max(1.0,\; \text{combinedSlope})$。时间序列惯例为 landscape;banking 应在斜率陡时推宽,但永不 portrait — 典型时间序列中 Gentle slope 多数会主导中位数并产生 portrait,压缩时间轴。 + +**Scatter plots**(非 connected):不用线斜率,而用归一化坐标的标准差比 $\sigma_x / \sigma_y$,阻尼响应:$\text{AR} = 1 + 0.3 \times (\text{sdRatio} - 1)$。 + +**无额外阻尼。** 原始 combined slope 直接返回,无乘性阻尼。与 gas pressure 的 50/50 混合(§3.8.4)是唯一 moderation;此处再加阻尼会 moderation 两次。 + +> **Implementation:** `compute-layout.ts` 中的 `computeBankingAR()`(约 819 行)。返回 W/H 宽高比于 $[0.5,\; 3.0]$。 + +### §3.8.4 Gas–Banking AR Blend + +Gas pressure 知道哪轴更拥挤(密度不对称)。Banking 知道感知理想 AR(斜率优化)。我们在**对数空间**等权混合两信号: + +$$\text{gasAR} = \frac{\text{rawW}}{\text{rawH}} \qquad \text{(from gas pressure per-axis stretches)}$$ + +$$\text{blendedAR} = \exp\!\left(0.5 \cdot \ln(\text{gasAR}) + 0.5 \cdot \ln(\text{bankingAR})\right)$$ + +这是几何平均:gas pressure 说 2:1(X 拥挤)且 banking 说 1:1(斜率平缓)时,混合得 $\sqrt{2} \approx 1.41$。 + +**Coverage gate:** 仅当 X、Y 数据各覆盖至少 20% 各自 domain 时应用 banking。数据集中在小区域(如一角簇)时斜率不可靠,仅 gas pressure 驱动 AR。 + +### §3.8.5 Area Budget and Shape + +混合决定 AR;gas pressure 决定总面积: + +$$\text{rawArea} = \text{rawW} \times \text{rawH}$$ + +封顶以防止子图在 fit 步骤前超出 per-subplot 预算: + +$$\text{area} = \min(\text{rawArea},\; W_{\text{sub}} \times H_{\text{sub}} \times \beta)$$ + +分配面积以匹配 blended AR: + +$$\text{idealW} = \sqrt{\text{area} \times \text{blendedAR}} \qquad \text{idealH} = \sqrt{\text{area} / \text{blendedAR}}$$ + +### §3.8.6 Fit to Budget (Preserving AR) + +每子图硬上限:$W_0 \times \beta$ 总量,跨 facet 面板共享: + +$$\text{availW} = \frac{W_0 \cdot \beta - \text{fixPad}}{F_c} - \text{gap} \qquad \text{availH} = \frac{H_0 \cdot \beta - \text{fixPad}}{F_r} - \text{gap}$$ + +均匀缩小以保留 blended AR: + +$$\text{fitScale} = \min\!\left(\frac{\text{availW}}{\text{idealW}},\; \frac{\text{availH}}{\text{idealH}},\; 1\right)$$ + +$$\text{finalW} = \max(S_{\min},\; \text{idealW} \times \text{fitScale}) \qquad \text{finalH} = \max(S_{\min},\; \text{idealH} \times \text{fitScale})$$ + +均匀 `fitScale` 确保两轴不超预算同时保留 blended AR,最小尺寸极端除外。 + +### §3.8.7 Worked Example + +150 dates × 8 series × 3 column facets,显式 $\beta = 2.0$ 上限便于算术(base $400 \times 300$,line chart:$\sigma_x = 100$,$\sigma_y = 20`,`seriesCountAxis: auto → Y`,`facetElasticity = 0.3`): + +**Per-subplot baseline:** +- Facet stretch: $\lambda_f = \min(2, 3^{0.3}) = 1.35$ +- $W_{\text{sub}} = (400 \times 1.35) / 3 = 180$ px + +**Gas pressure**(相对 $180 \times 300$): +- X positional: 150 unique,$\sigma_{1d} = 10$ → $p = 8.33$ → raw stretch $= 8.33^{0.3} = 1.93$ +- Y series: 8 series,$\sigma = 20$ → $p = 0.53$ → raw stretch $= 1.0$ +- rawW $= 180 \times 1.93 = 347$,rawH $= 300 \times 1.0 = 300$,gasAR $= 1.16$ + +**Banking AR**(多尺度斜率):设 combinedSlope 得 bankingAR $= 1.8$(landscape)。 + +**Blend:** $\text{blendedAR} = \exp(0.5 \ln 1.16 + 0.5 \ln 1.8) = \sqrt{1.16 \times 1.8} = 1.44$ + +**Area:** rawArea $= 347 \times 300 = 104{,}100$。maxArea $= 180 \times 300 \times 2 = 108{,}000$。area $= 104{,}100$。 +- idealW $= \sqrt{104100 \times 1.44} = 387$,idealH $= \sqrt{104100 / 1.44} = 269$ + +**Fit:** availW $= (800 - 0) / 3 = 267$,availH $= 600$。 +- fitScale $= \min(267/387, 600/269, 1) = 0.69$ +- finalW $= 387 \times 0.69 = 267$,finalH $= 269 \times 0.69 = 186$ +- **Final: 267 × 186, AR = 1.44** ✓ landscape preserved,total width = 800 + +> **Implementation:** `core/compute-layout.ts` 中的 `computeLayout()` — cont×cont path(约 370–530 行)。`computeBankingAR()`(约 819 行)。`core/decisions.ts` 中的 `computeGasPressure()`。 + +## §3.9 Band AR Blending + +### §3.9.1 问题 + +一轴 banded、另一轴 continuous 时(如 X 类别、Y 数值的 bar chart),§2 的 step size 决定 band 宽度,continuous 轴用默认画布高度。类别少而画布高时,每个 band 过度拉长。标签拥挤,bar 比例失真,浪费垂直空间。 + +### §3.9.2 Target Band AR + +**Band aspect ratio** 是 continuous 维与 step size 之比: + +$$\text{bandAR} = \frac{\text{continuousDim}}{\text{stepSize}}$$ + +`bandAR` 大(如 20:1)时每个 bar 宽 20 倍于高 — 视觉极端。`targetBandAR` 参数(默认:10)定义最大可接受比。 + +### §3.9.3 Log-Space Blend + +实际 band AR 超过 target 时,continuous 轴通过 50/50 对数空间混合(同 §3.8.4 机制)向 ideal **缩小**: + +$$\text{idealDim} = \text{stepSize} \times \text{targetBandAR}$$ + +$$\text{blendedDim} = \exp\!\left(0.5 \cdot \ln(\text{actualDim}) + 0.5 \cdot \ln(\text{idealDim})\right)$$ + +结果钳制到 $[S_{\min},\; \text{actualDim}]$ — 混合仅**缩小**,不增大。`bandAR ≤ targetBandAR` 时不调整。 + +### §3.9.4 Orientation Handling + +| Axis layout | Band AR formula | Adjusted dimension | +|---|---|---| +| X banded, Y continuous | $H / \text{xStep}$ | Shrink $H$ | +| Y banded, X continuous | $W / \text{yStep}$ | Shrink $W$ | + +### §3.9.5 Worked Example + +X 上 5 类别,step = 40 px,canvas height = 300 px,targetBandAR = 10: + +- bandAR $= 300 / 40 = 7.5 \leq 10$ → **no adjustment**。 + +X 上 3 类别,step = 60 px,canvas height = 300 px,targetBandAR = 10: + +- bandAR $= 300 / 60 = 5.0 \leq 10$ → **no adjustment**。 + +X 上 20 类别,step = 12 px,canvas height = 300 px,targetBandAR = 10: + +- bandAR $= 300 / 12 = 25 > 10$ → blend。 +- idealH $= 12 \times 10 = 120$。 +- blendedH $= \exp(0.5 \ln 300 + 0.5 \ln 120) = \sqrt{300 \times 120} = 190$ px。 +- **Result: height shrinks from 300 → 190.** + +> **Implementation:** `computeLayout()` 中的 Band AR blending 块(约 702–735 行)。由 `options.targetBandAR`(`AssembleOptions`)控制。VL backend 在 `assemble.ts` 中设默认 `targetBandAR = 10`。 + +--- + +# §4 圆周(Radial Pressure Model) + +## §4.1 问题 + +Radial 图表(Pie、Rose、Sunburst、Radar)将数据项排列在**圆**周上,相关维度是**圆周**。许多项拥挤圆周时,图表增长以保持 slice 和 spoke 可读。 + +**为何轴模型不适用:** +- **§2 (Spring):** 假设有端点的 1D 轴。Radial 图表是闭合环 — 增长意味着增大**半径**,圆周为 $C = 2\pi r$。 +- **§3 (Gas):** 假设 2D 自由浮点。Radial 项角向约束在 slice/spoke 位置。 + +Circumference 模型将 spring 直觉映射到极坐标:把圆周当作「弯轴」并 stretch 半径。 + +## §4.2 参数 + +| Symbol | Meaning | Default | +|---|---|---| +| $r_0$ | Base radius: $\max(r_{\min},\; \min(W_0, H_0)/2 - m)$ | derived | +| $C_0$ | Base circumference: $2\pi r_0$ | derived | +| $N_{\text{eff}}$ | Effective item count (§4.3) | data-dependent | +| $\ell_{\text{arc}}$ | Minimum arc-length per item (px) | 45 | +| $\alpha$ | Elasticity exponent | 0.5 | +| $\beta$ | Per-dimension max stretch | 2.0 | +| $r_{\min}$ | Minimum radius | 60 px | +| $r_{\max}$ | Maximum radius (absolute cap) | 400 px | +| $m$ | Margin around circle (px) | 20 | + +> **Code defaults:** `core/decisions.ts` 中的 `CircumferencePressureParams` — `minArcPx: 45`、`minRadius: 60`、`maxRadius: 400`、`elasticity: 0.5`、`maxStretch: 2.0`、`margin: 20`。 + +## §4.3 有效项数 + +不同 radial chart type 拥挤方式不同。模型将这些差异抽象为单一数字 $N_{\text{eff}}$。 + +**Uniform slices/spokes**(Rose、Radar):$N_{\text{eff}} = N$。 + +**Variable-width slices**(Pie、Sunburst): + +$$N_{\text{eff}} = \frac{\sum v_i}{\min(v_i)}$$ + +回答:「需要多少个最小 slice 才能填满整圆?」结果上限 100 以防退化。 + +**Sunburst:** 在**外环**(仅叶节点)计算 $N_{\text{eff}}$ — 最拥挤环。 + +> **Implementation:** `core/decisions.ts` 中的 `computeEffectiveBarCount()`(约 906–920 行)。 + +## §4.4 Pressure 与 Stretch + +**Pressure:** + +$$p = \frac{N_{\text{eff}} \cdot \ell_{\text{arc}}}{C_0} = \frac{N_{\text{eff}} \cdot \ell_{\text{arc}}}{2\pi r_0}$$ + +**Effective max stretch**(尊重 per-dimension 画布上限): + +$$s_{\max} = \min\!\left(\frac{r_{\max}}{r_0},\; \frac{\min(W_0 \cdot \beta,\; H_0 \cdot \beta) - 2m}{2 r_0}\right)$$ + +**Stretch:** + +$$ +s = \begin{cases} +1 & \text{if } p \leq 1 \cr +\min(s_{\max},\; p^{\alpha}) & \text{if } p > 1 +\end{cases} +$$ + +**Radius:** $r = \text{clamp}(r_0 \cdot s,\; r_{\min},\; r_{\max})$ + +> **Implementation:** `core/decisions.ts` 中的 `computeCircumferencePressure()`(约 850–893 行)。 + +## §4.5 画布尺寸 + +计算最终半径 $r$ 后: + +$$W = \max(W_0,\; 2r + 2m), \quad H = \max(H_0,\; 2r + 2m)$$ + +两画布维度等量增长(保持圆形宽高比)。 + +## §4.6 Gauge 分面 + +Gauge 图表是特例:每个 gauge 是单项 radial 图表。多个 gauge 由模板计算 facet 风格网格布局,因为 assembler 的 facet path 不适用于无轴图表。 + +所有 gauge 元素尺寸随计算半径**连续**缩放: + +$$\text{elementSize} = \text{baseline} \times (r / r_{\text{ref}})$$ + +其中 $r_{\text{ref}} = 100$ px。各元素钳制到最小值。避免阈值伪影。 + +## §4.7 参数表 + +| Chart type | $N_{\text{eff}}$ source | $\ell_{\text{arc}}$ | $\alpha$ | $\beta$ | $m$ | +|---|---|---|---|---|---| +| **Pie** | `total / min(values)` | 45 | 0.5 | 2.0 | 50 | +| **Rose** | N categories | 45 | 0.5 | 2.0 | 20 | +| **Sunburst** | outer-ring `total / min` | 45 | 0.5 | 2.0 | 20 | +| **Radar** | N spokes | 45 | 0.5 | 2.0 | 20 | +| **Gauge** | N dials (facet grid) | — | — | 2.0 | 20 | + +## §4.8 摘要 + +``` +Given: N_eff items, minArc ℓ_arc, base canvas W₀×H₀, + margin m, elasticity α, maxStretch β, minRadius, maxRadius + +r₀ = max(minRadius, (min(W₀, H₀) / 2) - m) +C₀ = 2π · r₀ +p = N_eff · ℓ_arc / C₀ + +// Effective max stretch on radius (per-dimension cap) +s_max = min(maxRadius / r₀, + (min(W₀·β, H₀·β) - 2m) / (2·r₀)) + +if p ≤ 1: + r = r₀ +else: + r = r₀ · min(s_max, p^α) + +r = clamp(r, minRadius, maxRadius) +W = max(W₀, 2r + 2m) +H = max(H₀, 2r + 2m) +``` + +> **Key functions:** `core/decisions.ts` 中的 `computeCircumferencePressure()`、`computeEffectiveBarCount()`。 + +--- + +# §5 面积布局(2D Pressure Model) + +## §5.1 问题 + +面积填充图表(如 Treemap)将 2D 画布划分为面积编码值的矩形。与 Cartesian 图表不同,根本资源是**总面积**。许多项拥挤时,每项过小无法标签或视觉区分。 + +**为何其他模型不适用:** +- **§2 / §3:** 独立推理 1D 轴。Treemap 项在任轴上无稳定位置;squarify 算法即时决定划分。 +- **§4:** 推理闭合环。Treemap 项占据 2D 面积,非角向扇区。 + +## §5.2 参数 + +| Symbol | Meaning | Default | +|---|---|---| +| $W_0, H_0$ | Base canvas dimensions | from context | +| $A_0$ | Base canvas area: $W_0 \times H_0$ | derived | +| $N_{\text{eff}}$ | Effective item count (§5.3) | data-dependent | +| $\ell_{\min}$ | Minimum width per effective item (px) | 30 | +| $\alpha$ | Elasticity exponent | 0.5 | +| $\beta$ | Per-dimension max stretch | 2.0 | +| $b$ | X-bias factor | 1.5 | + +> **Implementation note:** 面积模型目前**内联**实现在 `echarts/templates/treemap.ts`(约 91–115 行),非 `decisions.ts` 中的共享 core 函数。公式与默认值与本文完全一致。 + +## §5.3 有效项数 + +与 §4.3 相同公式: + +$$N_{\text{eff}} = \min\!\left(100,\; \frac{\sum v_i}{\min(v_i)}\right)$$ + +捕获最坏情况:需要多少最小项副本才能填满整个空间。 + +> **Implementation:** 调用 `core/decisions.ts` 中的 `computeEffectiveBarCount()`。 + +## §5.4 Pressure 与偏置分割 + +### Step 1: 1D Pressure + +想象所有 treemap 项沿 X 排成竖条。pressure 相对基准宽度衡量: + +$$p = \frac{N_{\text{eff}} \cdot \ell_{\min}}{W_0}$$ + +### Step 2: Area stretch + +$$ +A_{\text{stretch}} = \begin{cases} +1 & \text{if } p \leq 1 \cr +\min(\beta^2,\; p^{\alpha}) & \text{if } p > 1 +\end{cases} +$$ + +上限为 $\beta^2$,因为 $A = W \times H$ 且各维上限为 $\beta$。 + +### Step 3: Biased split to X and Y + +X 获得更多 stretch,因为阅读多为从左到右,标签为水平。 + +给定 X-bias factor $b$: + +$$s_x = \min(\beta,\; A_{\text{stretch}}^{\,b/(b+1)})$$ +$$s_y = \min(\beta,\; A_{\text{stretch}}^{\,1/(b+1)})$$ + +**Invariant:** $s_x \times s_y = A_{\text{stretch}}$。 + +| $b$ | X share | Y share | Effect | +|---|---|---|---| +| 1.0 | 50% | 50% | Uniform: $s_x = s_y = \sqrt{A_{\text{stretch}}}$ | +| 1.5 (default) | 60% | 40% | X takes more | +| 2.0 | 67% | 33% | Strongly X-biased | + +### Step 4: Canvas sizing + +$$W = \lfloor W_0 \cdot s_x \rceil, \quad H = \lfloor H_0 \cdot s_y \rceil$$ + +## §5.5 算例 + +Base canvas 400×300,$\ell_{\min} = 30$,$\alpha = 0.5$,$\beta = 2.0$,$b = 1.5$: + +| Scenario | $N_{\text{eff}}$ | Pressure | $A_{\text{stretch}}$ | $s_x$ | $s_y$ | W | H | +|---|---|---|---|---|---|---|---| +| 5 equal items | 5 | 0.38 | 1.0 | 1.0 | 1.0 | 400 | 300 | +| 10 equal items | 10 | 0.75 | 1.0 | 1.0 | 1.0 | 400 | 300 | +| 20 equal items | 20 | 1.50 | 1.22 | 1.13 | 1.08 | 452 | 324 | +| 50 equal items | 50 | 3.75 | 1.94 | 1.52 | 1.27 | 608 | 381 | +| Skewed (1 large + 20 tiny) | 100 | 7.50 | 2.74 | 1.87 | 1.46 | 748 | 438 | + +**为何偏置分割?** Treemap squarify 在画布接近正方形时产生近方形单元。给 X 更多 stretch 优先水平可读性:treemap 单元内标签为水平,额外宽度对容纳文字更有价值。 + +## §5.6 摘要 + +| Symbol | Meaning | Default | +|---|---|---| +| $N_{\text{eff}}$ | Effective item count ($\sum v / \min v$, cap 100) | data-dependent | +| $\ell_{\min}$ | Minimum width per effective item (px) | 30 | +| $\alpha$ | Elasticity exponent | 0.5 | +| $\beta$ | Per-dimension max stretch | 2.0 | +| $b$ | X-bias factor (1 = uniform, >1 = X takes more) | 1.5 | + +``` +Given: leaf values, base canvas W₀×H₀, + minBarPx, elasticity α, maxStretch β, xBias b + +N_eff = min(100, sum(values) / min(values)) +p = N_eff · minBarPx / W₀ + +if p ≤ 1: + A_stretch = 1 +else: + A_stretch = min(β², p^α) + +s_x = min(β, A_stretch^(b/(b+1))) +s_y = min(β, A_stretch^(1/(b+1))) + +W = round(W₀ · s_x) +H = round(H₀ · s_y) +``` + +> **Key function:** 内联于 `echarts/templates/treemap.ts`。使用 `core/decisions.ts` 中的 `computeEffectiveBarCount()`。 + +--- + +# §6 统一摘要 + +四个模型将同一核心思想 **pressure → elastic stretch → clamped output** 适配到不同几何上下文: + +| § | Model | Geometry | Pressure formula | Stretch dimension(s) | Chart types | +|---|---|---|---|---|---| +| §2 | Elastic Budget | 1D axis | $N \cdot \ell_0 / L_0$ | 1D (axis length) | Bar, Histogram, Heatmap, Boxplot | +| §3 | Gas Pressure | 2D point cloud | $\text{uniquePos} \cdot \sigma_{1d} / \text{dim}$ | Per-axis (X, Y independent) | Scatter, Line, Area | +| §4 | Circumference | 1D closed loop | $N_{\text{eff}} \cdot \ell_{\text{arc}} / C_0$ | Radius (both W, H equally) | Pie, Rose, Sunburst, Radar, Gauge | +| §5 | Area | 2D filled space | $N_{\text{eff}} \cdot \ell_{\min} / W_0$ | Area (biased X/Y split) | Treemap | + +### 共享概念 + +1. **Pressure = demand / supply.** 项需要空间;基准画布提供。Pressure > 1 表示溢出。 +2. **Elastic stretch.** $s = \min(\beta,\; p^\alpha)$。幂律指数 $\alpha$ 控制图表增长激进程度(gas 为 0.3,discrete/radial/area 为 0.5)。 +3. **Per-dimension cap $\beta$.** 无轴增长超过 $\beta \times$ 基准。radial/area 模型转化为半径或面积上限。 +4. **Effective item count.** 可变宽度项(pie、treemap)中,$N_{\text{eff}} = \sum v_i / \min(v_i)$ 衡量最坏拥挤。 + +### AR-aware extensions (§3.8–§3.9) + +连续轴上,原始 pressure 辅以宽高比智能: + +5. **Banking AR (§3.8.3).** 多尺度斜率分析(Heer & Agrawala 2006)决定感知理想 W/H 比。Connected marks 有 landscape floor(AR ≥ 1)。Scatter 用 σ-ratio。 +6. **Gas–Banking blend (§3.8.4).** 对数空间 50/50 几何平均:gasAR(密度)× bankingAR(感知)。 +7. **Per-subplot baseline (§3.8.2).** 分面图表向 gas pressure 输入 per-subplot 画布(含 facet elasticity),而非全画布。 +8. **Band AR blending (§3.9).** 一轴 banded、另一 continuous 时,`targetBandAR` 通过对数空间混合防止 band 过度拉长。 + +### 决策树 + +``` +Is the chart axis-based? +├── YES: Does it have banded (discrete) axes? +│ ├── Both banded → §2 Elastic Budget on each axis +│ ├── One banded → §2 for banded axis, §3 for continuous axis +│ │ + §3.9 Band AR blending if targetBandAR set +│ └── Neither → §3 Gas Pressure (both axes continuous) +│ + §3.8 Banking AR + Gas–Banking blend +└── NO: Is the layout radial (items around a circle)? + ├── YES → §4 Circumference Model + └── NO → §5 Area Model (2D space-filling) +``` + +### 实现映射 + +| Function | File | Model | +|---|---|---| +| `computeElasticBudget()` | `core/decisions.ts` | §2 | +| `computeAxisStep()` | `core/decisions.ts` | §2 | +| `computeGasPressure()` | `core/decisions.ts` | §3 | +| `computeBankingAR()` | `core/compute-layout.ts` | §3.8 | +| `computeCircumferencePressure()` | `core/decisions.ts` | §4 | +| `computeEffectiveBarCount()` | `core/decisions.ts` | §4, §5 | +| `computeLayout()` | `core/compute-layout.ts` | §2, §3, §3.8, §3.9 orchestration | +| `computeFacetGrid()` | `core/compute-layout.ts` | §2.8 faceting, §3.8 min subplot | +| `computeChannelBudgets()` | `core/compute-layout.ts` | §2.8 overflow budgets | +| Area pressure (inline) | `echarts/templates/treemap.ts` | §5 | diff --git a/docs/zh-CN/overview.md b/docs/zh-CN/overview.md new file mode 100644 index 00000000..7856489b --- /dev/null +++ b/docs/zh-CN/overview.md @@ -0,0 +1,182 @@ +# 概览 + +**Flint** 是一种面向数据可视化的语义驱动中间语言(IL)。你声明每个字段的*含义*以及想要的图表;编译器会推导比例尺、坐标轴、聚合、格式化、布局和颜色,然后输出 Vega-Lite、ECharts 或 Chart.js。 + +如果你是 Flint 新手,请从[入门指南](/documentation/getting-started)开始,然后再回到这里了解架构与 API 地图。 + +--- + +## 目录 + +- [§1 Flint 是什么](#1-what-flint-is) +- [§2 问题背景](#2-the-problem) +- [§3 Flint 规范](#3-flint-specification) +- [§4 编译器输出](#4-compiler-output) +- [§5 架构一览](#5-architecture-at-a-glance) +- [§6 文档地图](#6-documentation-map) +- [§7 安装与快速开始](#7-install-and-quick-start) +- [§8 本站工具](#8-tools-on-this-site) +- [§9 延伸阅读](#9-further-reading) + +--- + +# §1 Flint 是什么 + +Flint 将**数据语义**与**图表意图**分离,就像中间语言将程序逻辑与目标机器代码分离一样。作者无需手动微调相互依赖的低层参数,LLM 智能体也可以输出紧凑的 Flint 程序,而不是冗长、重新生成成本高、且在小改动下易碎的原生规范。 + +--- + +# §2 问题背景 + +当原始数据类型与视觉映射一致时,声明式语法(Vega-Lite、ECharts 等)效果很好。但当**语义含义**与存储表示不一致时,它们会变得脆弱: + +- 整数 `202001` 表示 **YearMonth**,而非定量幅度 +- 堆叠不可加性度量(温度、比率) +- 在顺序色带上使用发散型字段 + +专家可以用冗长、耦合的规范修复这些情况,但在更换字段、旋转热力图或更改图表类型时,这些规范很难保持正确。Flint 将**语义类型视为一等对象**,并根据语义与数据特征解析编码和布局。 + +--- + +# §3 Flint 规范 + +一个 Flint 程序有两个可复用部分: + +| Flint 术语 | API 字段 | 作用 | +|------------|-----------|------| +| **dataSpec** | `semantic_types` | 每个字段的含义 → 类型字符串或富注解 | +| **chartSpec** | `chart_spec` | 图表类型 + 通道 → 字段绑定 | + +原始行数据存放在 `data` 中。三者共同构成 `ChartAssemblyInput`: + +```text +data + semantic_types + chart_spec → assemble*() → native spec +``` + +### dataSpec 示例 + +注解**内联**在 `semantic_types` 中——没有单独的 `semantic_annotations` 字段: + +```json +{ + "semantic_types": { + "period": "YearMonth", + "game": "Category", + "gameType": "Category", + "newUsers": "PercentageChange", + "totalUsers": "Quantity", + "region": { + "semanticType": "Category", + "sortOrder": ["N", "E", "S", "W"] + } + } +} +``` + +### chartSpec 示例 + +分面折线图: + +```json +{ + "chart_spec": { + "chartType": "Line Chart", + "encodings": { + "column": { "field": "region" }, + "x": { "field": "period" }, + "y": { "field": "totalUsers" }, + "color": { "field": "gameType" } + }, + "baseSize": { "width": 480, "height": 320 } + } +} +``` + +**探索工作流:** 仅修改 `chart_spec` 即可尝试热力图、分组柱状图、瀑布图或旭日图。**dataSpec 保持不变**,你还可以切换后端(例如 Vega-Lite → ECharts)而无需重写 Flint 输入。模板与后端覆盖范围请见[图库](/gallery)。 + +语义类型采用三级层次结构。详情见[语义类型](/documentation/semantic-types)。 + +--- + +# §4 编译器输出 + +| 函数 | 输出 | +|----------|--------| +| `assembleVegaLite(input)` | Vega-Lite v6 spec | +| `assembleECharts(input)` | ECharts `option` | +| `assembleChartjs(input)` | Chart.js config | + +同一输入可编译到所有支持的后端。阶段 1–2(语义 + 布局)位于共享的 `core/`;仅阶段 3(模板实例化)因库而异。 + +完整输入模式见 [API 参考](/documentation/api-reference)。 + +--- + +# §5 架构一览 + +![Overview of the Flint architecture](figs/overview.png) + +| Flint 阶段 | 代码 | 模块 | +|-------------|------|--------| +| **编译器前端** | Phase 0 — `resolveChannelSemantics()` | `core/resolve-semantics.ts` | +| **优化器** | Phase 1 — `computeLayout()`、overflow filter | `core/compute-layout.ts` | +| **代码生成器** | Phase 2 — `template.instantiate()` | `vegalite/`、`echarts/`、`chartjs/` | + +1. **前端** — 从 dataSpec + data 推导编码类型、格式、聚合、比例尺、域、颜色和排序 +2. **优化器** — 用基于物理的尺寸选择坐标轴跨度、带宽步长、分面网格和宽高比;先从[示例:自动布局](/documentation/chart-sizing)入手,再用[自动布局算法](/documentation/layout-model)了解公式 +3. **代码生成器** — 为每个 `chartType` 使用动态模板,输出库原生规范 + +流水线详情见[架构](/documentation/architecture)。 + +--- + +# §6 文档地图 + +| 章节 | 页面 | +|---------|-------| +| **语言设计** | [架构](/documentation/architecture)、[语义类型](/documentation/semantic-types)、[自动布局算法](/documentation/layout-model)、[API 参考](/documentation/api-reference) | +| **图表参考** | [Vega-Lite 图表](/documentation/reference-vegalite)、[ECharts 图表](/documentation/reference-echarts)、[Chart.js 图表](/documentation/reference-chartjs) | +| **开发** | [开发指南](/documentation/development)、[扩展语义类型](/documentation/adding-a-semantic-type)、[扩展后端](/documentation/adding-a-backend)、[扩展图表模板](/documentation/adding-a-chart-template) | + +--- + +# §7 安装与快速开始 + +```bash +npm install flint-chart # JavaScript / TypeScript +npx -y flint-chart-mcp # MCP server for agents + +# Python/PyPI is planned for a later release. +``` + +```ts +import { assembleVegaLite } from 'flint-chart'; + +const spec = assembleVegaLite({ + data: { values: [{ quarter: 'Q1', revenue: 1200 }] }, + semantic_types: { quarter: 'Quarter', revenue: 'Price' }, + chart_spec: { + chartType: 'Bar Chart', + encodings: { x: { field: 'quarter' }, y: { field: 'revenue' } }, + baseSize: { width: 480, height: 320 }, + }, +}); +``` + +--- + +# §8 本站工具 + +| 页面 | 用途 | +|------|---------| +| [入门指南](/documentation/getting-started) | 分步创建第一张图表 | +| [配置 Flint MCP](/documentation/setup-flint-mcp) | MCP 服务器配置、文件访问、工具与验证 | +| [智能体工作流](/documentation/agent-workflows) | 自定义智能体与产品集成模式 | +| [图库](/gallery) | 所有模板 + 多后端预览 | +| [编辑器](/editor) | 粘贴 JSON,切换 Vega-Lite / ECharts / Chart.js | + +--- + +# §9 延伸阅读 + +- 面向智能体的设计说明:[docs/README.md](https://github.com/microsoft/flint-chart/blob/main/docs/README.md) diff --git a/docs/zh-CN/reference-chartjs.md b/docs/zh-CN/reference-chartjs.md new file mode 100644 index 00000000..34945e48 --- /dev/null +++ b/docs/zh-CN/reference-chartjs.md @@ -0,0 +1,194 @@ +# Chart.js 图表参考 + +> 本页由实时 chart-template 注册表(`scripts/gen-chart-reference.ts`)生成。请勿手工编辑 — 请运行 `npm run gen:reference`。 + +Chart.js 后端是常见图表族的轻量嵌入目标,有意保持较小的参数面。 + +## 本页内容 + +本参考列出 Chart.js 后端当前支持的 20 种图表类型,分为 5 个类别。每个图表条目包含: + +- **编码通道** — `chart_spec.encodings` 中接受的视觉角色,例如 `x`、`y`、`color`、`size`、`column` 或 `row`。 +- **选项** — 模板专属的 `chart_spec.chartProperties` 键,包括控件类型、域、默认值、可用性与描述。 + +请严格按照 `chart_spec.chartType` 中显示的名称使用图表类型。 + +## 如何设置 encodings 与 options + +在 `chart_spec.encodings` 中设置 encodings,在 `chart_spec.chartProperties` 中设置图表专属选项。选项键与下方参数名一致: + +```jsonc +{ + "chartType": "Bar Chart", + "encodings": { "x": { "field": "category" }, "y": { "field": "value" } }, + "chartProperties": { "cornerRadius": 4, "stackMode": "normalize" } +} +``` + +**可用性**列表示参数是 `always`(始终可用)还是 `conditional`(条件可用);后者仅在数据与 encodings 使其相关时出现。例如,对数比例尺控件仅出现在范围较宽的轴上。传入不适用的参数是安全的;组装器会忽略它们。 + +## 散点与点图 + +### ![](chart-icon-scatter.svg) Scatter Plot + +**编码通道:** `x`, `y`, `color`, `size`, `opacity`, `column`, `row` + +| 参数 | 控件 | 取值范围 | 默认值 | 可用性 | 描述 | +|---|---|---|---|---|---| +| `opacity` | number | 0.1 – 1 (step 0.05) | `1` | always | 标记不透明度。 | + +### ![](chart-icon-connected-scatter.svg) Connected Scatter Plot + +**编码通道:** `x`, `y`, `order`, `color`, `detail`, `column`, `row` + +_无模板专属参数。_ + +### ![](chart-icon-bubble.svg) Bubble Chart + +**编码通道:** `x`, `y`, `size`, `color`, `opacity`, `column`, `row` + +| 参数 | 控件 | 取值范围 | 默认值 | 可用性 | 描述 | +|---|---|---|---|---|---| +| `opacity` | number | 0.1 – 1 (step 0.05) | `0.6` | always | 标记不透明度。 | + +### ![](chart-icon-strip-plot.svg) Strip Plot + +**编码通道:** `x`, `y`, `color`, `size`, `column`, `row` + +| 参数 | 控件 | 取值范围 | 默认值 | 可用性 | 描述 | +|---|---|---|---|---|---| +| `stepWidth` | number | 10 – 100 (step 5) | `20` | always | 抖动散布宽度。 | +| `pointSize` | number | 0 – 150 (step 5) | `0` | always | 点或标记大小。 | +| `opacity` | number | 0 – 1 (step 0.05) | `0` | always | 标记不透明度。 | + +## 条形图 + +### ![](chart-icon-column.svg) Bar Chart + +**编码通道:** `x`, `y`, `color`, `opacity`, `column`, `row` + +| 参数 | 控件 | 取值范围 | 默认值 | 可用性 | 描述 | +|---|---|---|---|---|---| +| `cornerRadius` | number | 0 – 15 (step 1) | `0` | always | 支持的标记的圆角半径。 | + +### ![](chart-icon-column-grouped.svg) Grouped Bar Chart + +**编码通道:** `x`, `y`, `group`, `color`, `column`, `row` + +| 参数 | 控件 | 取值范围 | 默认值 | 可用性 | 描述 | +|---|---|---|---|---|---| +| `dodge` | choice | `auto` (Auto), `local` (Local (compact)), `global` (Global (aligned)) | `auto` | conditional | Dodge | + +### ![](chart-icon-column-stacked.svg) Stacked Bar Chart + +**编码通道:** `x`, `y`, `color`, `column`, `row` + +_无模板专属参数。_ + +### ![](chart-icon-combo.svg) Combo Chart + +**编码通道:** `x`, `y`, `column`, `row` + +| 参数 | 控件 | 取值范围 | 默认值 | 可用性 | 描述 | +|---|---|---|---|---|---| +| `cornerRadius` | number | 0 – 15 (step 1) | `0` | always | 支持的标记的圆角半径。 | + +### ![](chart-icon-histogram.svg) Histogram + +**编码通道:** `x`, `color`, `column`, `row` + +| 参数 | 控件 | 取值范围 | 默认值 | 可用性 | 描述 | +|---|---|---|---|---|---| +| `binCount` | number | 5 – 50 (step 1) | `Auto` | always | 最大分箱上限;Auto 由后端选择。 | + +### ![](chart-icon-waterfall.svg) Waterfall Chart + +**编码通道:** `x`, `y`, `color`, `column`, `row` + +_无模板专属参数。_ + +### ![](chart-icon-gantt.svg) Gantt Chart + +**编码通道:** `y`, `x`, `x2`, `color`, `column`, `row` + +_无模板专属参数。_ + +## 折线与面积 + +### ![](chart-icon-line.svg) Line Chart + +**编码通道:** `x`, `y`, `color`, `opacity`, `column`, `row` + +| 参数 | 控件 | 取值范围 | 默认值 | 可用性 | 描述 | +|---|---|---|---|---|---| +| `interpolate` | choice | Default (linear) _(default)_, `linear` (Linear), `monotone` (Monotone (smooth)), `step` (Step), `step-before` (Step Before), `step-after` (Step After) | — | always | 折线或面积插值方法。 | + +### ![](chart-icon-slope.svg) Slope Chart + +**编码通道:** `x`, `y`, `color`, `detail`, `column`, `row` + +_无模板专属参数。_ + +### ![](chart-icon-area.svg) Area Chart + +**编码通道:** `x`, `y`, `color`, `opacity`, `column`, `row` + +| 参数 | 控件 | 取值范围 | 默认值 | 可用性 | 描述 | +|---|---|---|---|---|---| +| `interpolate` | choice | Default (linear) _(default)_, `linear` (Linear), `monotone` (Monotone (smooth)) | — | always | 折线或面积插值方法。 | +| `opacity` | number | 0.1 – 1 (step 0.05) | `0.4` | always | 标记不透明度。 | +| `stackMode` | choice | Stacked (default) _(default)_, `layered` (Layered (overlap)) | — | always | 重叠系列的堆叠策略。 | + +### ![](chart-icon-range-area.svg) Range Area Chart + +**编码通道:** `x`, `y`, `y2`, `color`, `column`, `row` + +_无模板专属参数。_ + +### ![](chart-icon-ecdf.svg) ECDF Plot + +**编码通道:** `x`, `color`, `detail`, `column`, `row` + +| 参数 | 控件 | 取值范围 | 默认值 | 可用性 | 描述 | +|---|---|---|---|---|---| +| `showPoints` | toggle | on / off | `false` | always | 在线上叠加点标记。 | + +## 部分与整体 + +### ![](chart-icon-pie.svg) Pie Chart + +**编码通道:** `size`, `color`, `column`, `row` + +| 参数 | 控件 | 取值范围 | 默认值 | 可用性 | 描述 | +|---|---|---|---|---|---| +| `innerRadius` | number | 0 – 60 (step 5) | `0` | always | 内半径占外半径的百分比。 | +| `sortSlices` | choice | `none` (Data order), `descending` (Largest first), `ascending` (Smallest first) | `none` | always | 排序扇区 | + +### ![](chart-icon-doughnut.svg) Doughnut Chart + +**编码通道:** `size`, `color`, `column`, `row` + +| 参数 | 控件 | 取值范围 | 默认值 | 可用性 | 描述 | +|---|---|---|---|---|---| +| `innerRadius` | number | 20 – 80 (step 5) | `55` | always | 内半径占外半径的百分比。 | +| `sortSlices` | choice | `none` (Data order), `descending` (Largest first), `ascending` (Smallest first) | `none` | always | 排序扇区 | + +## 极坐标 + +### ![](chart-icon-radar.svg) Radar Chart + +**编码通道:** `x`, `y`, `color`, `column`, `row` + +| 参数 | 控件 | 取值范围 | 默认值 | 可用性 | 描述 | +|---|---|---|---|---|---| +| `filled` | choice | `true` (Filled (default)), `false` (Outline only) | — | always | 填充雷达图封闭区域。 | +| `fillOpacity` | number | 0.05 – 0.8 (step 0.05) | `0.3` | always | 面积或区域的填充不透明度。 | + +### ![](chart-icon-rose.svg) Rose Chart + +**编码通道:** `x`, `y`, `color`, `column`, `row` + +| 参数 | 控件 | 取值范围 | 默认值 | 可用性 | 描述 | +|---|---|---|---|---|---| +| `alignment` | choice | `left` (Left (default)), `center` (Center) | — | always | 径向图的段对齐方式。 | +| `sortSlices` | choice | `none` (Data order), `descending` (Largest first), `ascending` (Smallest first) | `none` | always | 排序扇区 | diff --git a/docs/zh-CN/reference-echarts.md b/docs/zh-CN/reference-echarts.md new file mode 100644 index 00000000..1a6647b7 --- /dev/null +++ b/docs/zh-CN/reference-echarts.md @@ -0,0 +1,336 @@ +# ECharts 图表参考 + +> 本页由实时 chart-template 注册表(`scripts/gen-chart-reference.ts`)生成。请勿手工编辑 — 请运行 `npm run gen:reference`。 + +ECharts 后端面向交互式、canvas 渲染的图表,并覆盖 Vega-Lite 范围之外的若干结构:sunburst、treemap、sankey、gauge、graph、tree、parallel coordinates 与 calendar heatmap。 + +## 本页内容 + +本参考列出 ECharts 后端当前支持的 37 种图表类型,分为 10 个类别。每个图表条目包含: + +- **编码通道** — `chart_spec.encodings` 中接受的视觉角色,例如 `x`、`y`、`color`、`size`、`column` 或 `row`。 +- **选项** — 模板专属的 `chart_spec.chartProperties` 键,包括控件类型、域、默认值、可用性与描述。 + +请严格按照 `chart_spec.chartType` 中显示的名称使用图表类型。 + +## 如何设置 encodings 与 options + +在 `chart_spec.encodings` 中设置 encodings,在 `chart_spec.chartProperties` 中设置图表专属选项。选项键与下方参数名一致: + +```jsonc +{ + "chartType": "Bar Chart", + "encodings": { "x": { "field": "category" }, "y": { "field": "value" } }, + "chartProperties": { "cornerRadius": 4, "stackMode": "normalize" } +} +``` + +**可用性**列表示参数是 `always`(始终可用)还是 `conditional`(条件可用);后者仅在数据与 encodings 使其相关时出现。例如,对数比例尺控件仅出现在范围较宽的轴上。传入不适用的参数是安全的;组装器会忽略它们。 + +## 散点与点图 + +### ![](chart-icon-scatter.svg) Scatter Plot + +**编码通道:** `x`, `y`, `color`, `size`, `opacity`, `column`, `row` + +| 参数 | 控件 | 取值范围 | 默认值 | 可用性 | 描述 | +|---|---|---|---|---|---| +| `opacity` | number | 0.1 – 1 (step 0.05) | `1` | always | 标记不透明度。 | + +### ![](chart-icon-linear-regression.svg) Regression + +**编码通道:** `x`, `y`, `size`, `color`, `column`, `row` + +| 参数 | 控件 | 取值范围 | 默认值 | 可用性 | 描述 | +|---|---|---|---|---|---| +| `regressionMethod` | choice | `linear` (Linear), `log` (Logarithmic), `exp` (Exponential), `pow` (Power), `quad` (Quadratic), `poly` (Polynomial) | `linear` | always | 回归拟合方法。 | +| `polyOrder` | number | 2 – 10 (step 1) | `3` | always | 回归拟合的多项式阶数。 | + +### ![](chart-icon-connected-scatter.svg) Connected Scatter Plot + +**编码通道:** `x`, `y`, `order`, `color`, `detail`, `column`, `row` + +_无模板专属参数。_ + +### ![](chart-icon-dot-plot-horizontal.svg) Ranged Dot Plot + +**编码通道:** `x`, `y`, `color` + +_无模板专属参数。_ + +### ![](chart-icon-box-plot.svg) Boxplot + +**编码通道:** `x`, `y`, `color`, `opacity`, `column`, `row` + +| 参数 | 控件 | 取值范围 | 默认值 | 可用性 | 描述 | +|---|---|---|---|---|---| +| `whiskerMethod` | choice | `iqr` (Tukey (1.5 × IQR)), `minmax` (Min–Max) | `iqr` | always | 须线 | +| `showOutliers` | toggle | on / off | `true` | conditional | 异常值 | +| `dodge` | choice | `auto` (Auto), `local` (Local (compact)), `global` (Global (aligned)) | `auto` | conditional | Dodge | + +### ![](chart-icon-strip-plot.svg) Strip Plot + +**编码通道:** `x`, `y`, `color`, `size`, `column`, `row` + +_无模板专属参数。_ + +## 条形图 + +### ![](chart-icon-column.svg) Bar Chart + +**编码通道:** `x`, `y`, `color`, `opacity`, `column`, `row` + +| 参数 | 控件 | 取值范围 | 默认值 | 可用性 | 描述 | +|---|---|---|---|---|---| +| `cornerRadius` | number | 0 – 15 (step 1) | `0` | always | 支持的标记的圆角半径。 | + +### ![](chart-icon-column-grouped.svg) Grouped Bar Chart + +**编码通道:** `x`, `y`, `group`, `color`, `column`, `row` + +| 参数 | 控件 | 取值范围 | 默认值 | 可用性 | 描述 | +|---|---|---|---|---|---| +| `dodge` | choice | `auto` (Auto), `local` (Local (compact)), `global` (Global (aligned)) | `auto` | conditional | Dodge | + +### ![](chart-icon-column-stacked.svg) Stacked Bar Chart + +**编码通道:** `x`, `y`, `color`, `column`, `row` + +| 参数 | 控件 | 取值范围 | 默认值 | 可用性 | 描述 | +|---|---|---|---|---|---| +| `stackMode` | choice | Stacked (default) _(default)_, `normalize` (Normalize (100%)), `layered` (Layered (overlap)) | — | conditional | 重叠系列的堆叠策略。 | + +### ![](chart-icon-lollipop.svg) Lollipop Chart + +**编码通道:** `x`, `y`, `color`, `column`, `row` + +| 参数 | 控件 | 取值范围 | 默认值 | 可用性 | 描述 | +|---|---|---|---|---|---| +| `dotSize` | number | 20 – 300 (step 10) | `80` | always | 圆点标记的大小。 | + +### ![](chart-icon-pyramid.svg) Pyramid Chart + +**编码通道:** `x`, `y`, `color` + +_无模板专属参数。_ + +### ![](chart-icon-heat-map.svg) Heatmap + +**编码通道:** `x`, `y`, `color`, `column`, `row` + +_无模板专属参数。_ + +### ![](chart-icon-calendar.svg) Calendar Heatmap + +**编码通道:** `x`, `color` + +_无模板专属参数。_ + +## 折线与面积 + +### ![](chart-icon-line.svg) Line Chart + +**编码通道:** `x`, `y`, `color`, `opacity`, `column`, `row` + +| 参数 | 控件 | 取值范围 | 默认值 | 可用性 | 描述 | +|---|---|---|---|---|---| +| `interpolate` | choice | Default (linear) _(default)_, `linear` (Linear), `monotone` (Monotone (smooth)), `step` (Step), `step-before` (Step Before), `step-after` (Step After) | — | always | 折线或面积插值方法。 | +| `showPoints` | toggle | on / off | `false` | always | 在线上叠加点标记。 | + +### ![](chart-icon-bump.svg) Bump Chart + +**编码通道:** `x`, `y`, `color`, `detail`, `column`, `row` + +_无模板专属参数。_ + +### ![](chart-icon-slope.svg) Slope Chart + +**编码通道:** `x`, `y`, `color`, `detail`, `column`, `row` + +_无模板专属参数。_ + +### ![](chart-icon-area.svg) Area Chart + +**编码通道:** `x`, `y`, `color`, `opacity`, `column`, `row` + +| 参数 | 控件 | 取值范围 | 默认值 | 可用性 | 描述 | +|---|---|---|---|---|---| +| `interpolate` | choice | Default (linear) _(default)_, `linear` (Linear), `monotone` (Monotone (smooth)), `step` (Step), `step-before` (Step Before), `step-after` (Step After) | — | always | 折线或面积插值方法。 | +| `opacity` | number | 0.1 – 1 (step 0.05) | `0.7` | always | 标记不透明度。 | +| `stackMode` | choice | Stacked (default) _(default)_, `normalize` (Normalize (100%)), `center` (Center), `layered` (Layered (overlap)) | — | always | 重叠系列的堆叠策略。 | + +### ![](chart-icon-streamgraph.svg) Streamgraph + +**编码通道:** `x`, `y`, `color`, `column`, `row` + +_无模板专属参数。_ + +### ![](chart-icon-range-area.svg) Range Area Chart + +**编码通道:** `x`, `y`, `y2`, `color`, `column`, `row` + +_无模板专属参数。_ + +## 部分与整体 + +### ![](chart-icon-pie.svg) Pie Chart + +**编码通道:** `size`, `color`, `column`, `row` + +| 参数 | 控件 | 取值范围 | 默认值 | 可用性 | 描述 | +|---|---|---|---|---|---| +| `innerRadius` | number | 0 – 60 (step 5) | `0` | always | 内半径占外半径的百分比。 | +| `cornerRadius` | number | 0 – 10 (step 1) | `0` | always | 支持的标记的圆角半径。 | +| `sortSlices` | choice | `none` (Data order), `descending` (Largest first), `ascending` (Smallest first) | `none` | always | 排序扇区 | +| `labelType` | choice | `categoryPercent` (Name + %), `category` (Name), `value` (Value), `percent` (Percent), `none` (None) | `categoryPercent` | always | 标签 | + +### ![](chart-icon-funnel.svg) Funnel Chart + +**编码通道:** `y`, `size` + +| 参数 | 控件 | 取值范围 | 默认值 | 可用性 | 描述 | +|---|---|---|---|---|---| +| `sort` | choice | `descending` (Descending (default)), `ascending` (Ascending), `none` (Original order) | — | always | 有序阶段或类别的排序顺序。 | +| `orient` | choice | `vertical` (Vertical (default)), `horizontal` (Horizontal) | — | always | 图表方向。 | +| `gap` | number | 0 – 20 (step 1) | `2` | always | 段之间的间距。 | + +### ![](chart-icon-treemap.svg) Treemap + +**编码通道:** `color`, `size`, `detail` + +| 参数 | 控件 | 取值范围 | 默认值 | 可用性 | 描述 | +|---|---|---|---|---|---| +| `breadcrumb` | choice | `true` (Show (default)), `false` (Hide) | — | always | 显示或隐藏 treemap 面包屑导航。 | + +### ![](chart-icon-sunburst.svg) Sunburst Chart + +**编码通道:** `color`, `size`, `detail`, `group` + +| 参数 | 控件 | 取值范围 | 默认值 | 可用性 | 描述 | +|---|---|---|---|---|---| +| `innerRadius` | number | 0 – 80 (step 5) | `0` | always | 内半径占外半径的百分比。 | +| `labelRotate` | choice | `radial` (Radial (default)), `tangential` (Tangential), `0` (Horizontal) | — | always | sunburst 扇区的标签方向。 | + +### ![](chart-icon-tree.svg) Tree + +**编码通道:** `color`, `detail`, `size` + +| 参数 | 控件 | 取值范围 | 默认值 | 可用性 | 描述 | +|---|---|---|---|---|---| +| `orient` | choice | `LR` (Left → Right (default)), `TB` (Top → Bottom) | — | always | 图表方向。 | + +## 统计 + +### ![](chart-icon-histogram.svg) Histogram + +**编码通道:** `x`, `color`, `column`, `row` + +| 参数 | 控件 | 取值范围 | 默认值 | 可用性 | 描述 | +|---|---|---|---|---|---| +| `binCount` | number | 5 – 50 (step 1) | `Auto` | always | 最大分箱上限;Auto 由后端选择。 | + +### ![](chart-icon-density.svg) Density Plot + +**编码通道:** `x`, `color`, `column`, `row` + +| 参数 | 控件 | 取值范围 | 默认值 | 可用性 | 描述 | +|---|---|---|---|---|---| +| `bandwidth` | number | 0.05 – 2 (step 0.05) | `0` | always | 核密度带宽(0 = 自动)。 | + +### ![](chart-icon-ecdf.svg) ECDF Plot + +**编码通道:** `x`, `color`, `detail`, `column`, `row` + +| 参数 | 控件 | 取值范围 | 默认值 | 可用性 | 描述 | +|---|---|---|---|---|---| +| `showPoints` | toggle | on / off | `false` | always | 在线上叠加点标记。 | + +### ![](chart-icon-parallel.svg) Parallel Coordinates + +**编码通道:** `color`, `detail` + +_无模板专属参数。_ + +## 金融 + +### ![](chart-icon-candlestick.svg) Candlestick Chart + +**编码通道:** `x`, `open`, `high`, `low`, `close`, `column`, `row` + +| 参数 | 控件 | 取值范围 | 默认值 | 可用性 | 描述 | +|---|---|---|---|---|---| +| `showMA` | toggle | on / off | `false` | always | 显示移动平均线叠加层。 | +| `maWindow` | number | 3 – 30 (step 1) | `5` | always | 移动平均线窗口大小。 | + +## 其他 + +### ![](chart-icon-waterfall.svg) Waterfall Chart + +**编码通道:** `x`, `y`, `color`, `column`, `row` + +_无模板专属参数。_ + +### ![](chart-icon-gantt.svg) Gantt Chart + +**编码通道:** `y`, `x`, `x2`, `color`, `detail`, `column`, `row` + +_无模板专属参数。_ + +### ![](chart-icon-bullet.svg) Bullet Chart + +**编码通道:** `y`, `x`, `goal`, `color`, `column`, `row` + +_无模板专属参数。_ + +## 极坐标 + +### ![](chart-icon-radar.svg) Radar Chart + +**编码通道:** `x`, `y`, `color`, `column`, `row` + +| 参数 | 控件 | 取值范围 | 默认值 | 可用性 | 描述 | +|---|---|---|---|---|---| +| `shape` | choice | Polygon (default) _(default)_, `circle` (Circle) | — | always | 网格 | +| `filled` | choice | `true` (Filled (default)), `false` (Outline only) | — | always | 填充雷达图封闭区域。 | +| `fillOpacity` | number | 0.05 – 0.8 (step 0.05) | `0.3` | always | 面积或区域的填充不透明度。 | + +### ![](chart-icon-rose.svg) Rose Chart + +**编码通道:** `x`, `y`, `color`, `column`, `row` + +| 参数 | 控件 | 取值范围 | 默认值 | 可用性 | 描述 | +|---|---|---|---|---|---| +| `alignment` | choice | `left` (Left (default)), `center` (Center) | — | always | 径向图的段对齐方式。 | +| `sortSlices` | choice | `none` (Data order), `descending` (Largest first), `ascending` (Smallest first) | `none` | always | 排序扇区 | + +## 指标 + +### ![](chart-icon-gauge.svg) Gauge Chart + +**编码通道:** `size`, `column` + +| 参数 | 控件 | 取值范围 | 默认值 | 可用性 | 描述 | +|---|---|---|---|---|---| +| `min` | number | 0 – 1000 (step 10) | `0` | always | 最小值 | +| `max` | number | 0 – 10000 (step 100) | `100` | always | 最大值 | +| `showProgress` | choice | `true` (Show (default)), `false` (Hide) | — | always | 进度 | + +## 流向 + +### ![](chart-icon-sankey.svg) Sankey Diagram + +**编码通道:** `x`, `y`, `size` + +| 参数 | 控件 | 取值范围 | 默认值 | 可用性 | 描述 | +|---|---|---|---|---|---| +| `orient` | choice | `horizontal` (Horizontal (default)), `vertical` (Vertical) | — | always | 图表方向。 | +| `nodeWidth` | number | 5 – 40 (step 5) | `20` | always | 节点宽度 | +| `nodeGap` | number | 2 – 30 (step 2) | `10` | always | 节点间距 | + +### ![](chart-icon-network.svg) Network Graph + +**编码通道:** `x`, `y`, `size` + +| 参数 | 控件 | 取值范围 | 默认值 | 可用性 | 描述 | +|---|---|---|---|---|---| +| `layout` | choice | `circular` (Circular (default)), `force` (Force-directed) | — | always | 布局 | diff --git a/docs/zh-CN/reference-vegalite.md b/docs/zh-CN/reference-vegalite.md new file mode 100644 index 00000000..f932f08b --- /dev/null +++ b/docs/zh-CN/reference-vegalite.md @@ -0,0 +1,429 @@ +# Vega-Lite 图表参考 + +> 本页由实时 chart-template 注册表(`scripts/gen-chart-reference.ts`)生成。请勿手工编辑 — 请运行 `npm run gen:reference`。 + +Vega-Lite 后端是 Flint 的参考实现,提供最广的图表覆盖。当你需要最完整的声明式图表支持(包括轴、比例尺与分面行为)时,应使用它。 + +## 本页内容 + +本参考列出 Vega-Lite 后端当前支持的 34 种图表类型,分为 6 个类别。每个图表条目包含: + +- **编码通道** — `chart_spec.encodings` 中接受的视觉角色,例如 `x`、`y`、`color`、`size`、`column` 或 `row`。 +- **选项** — 模板专属的 `chart_spec.chartProperties` 键,包括控件类型、域、默认值、可用性与描述。 + +请严格按照 `chart_spec.chartType` 中显示的名称使用图表类型。 + +## 如何设置 encodings 与 options + +在 `chart_spec.encodings` 中设置 encodings,在 `chart_spec.chartProperties` 中设置图表专属选项。选项键与下方参数名一致: + +```jsonc +{ + "chartType": "Bar Chart", + "encodings": { "x": { "field": "category" }, "y": { "field": "value" } }, + "chartProperties": { "cornerRadius": 4, "stackMode": "normalize" } +} +``` + +**可用性**列表示参数是 `always`(始终可用)还是 `conditional`(条件可用);后者仅在数据与 encodings 使其相关时出现。例如,对数比例尺控件仅出现在范围较宽的轴上。传入不适用的参数是安全的;组装器会忽略它们。 + +## 点图 + +### ![](chart-icon-scatter.svg) Scatter Plot + +**编码通道:** `x`, `y`, `color`, `size`, `shape`, `opacity`, `column`, `row` + +| 参数 | 控件 | 取值范围 | 默认值 | 可用性 | 描述 | +|---|---|---|---|---|---| +| `opacity` | number | 0.1 – 1 (step 0.1) | `1` | always | 标记不透明度。 | +| `independentYAxis` | toggle | on / off | `false` | conditional | 为分面使用独立的 y 轴比例尺。 | +| `logScale_x` | toggle | on / off | `false` | conditional | 在 x 轴上使用对数/symlog 比例尺。 | +| `logScale_y` | toggle | on / off | `false` | conditional | 在 y 轴上使用对数/symlog 比例尺。 | +| `includeZero_x` | toggle | on / off | `false` | conditional | 将 x 轴锚定在零。 | +| `includeZero_y` | toggle | on / off | `false` | conditional | 将 y 轴锚定在零。 | + +### ![](chart-icon-linear-regression.svg) Regression + +**编码通道:** `x`, `y`, `size`, `color`, `column`, `row` + +| 参数 | 控件 | 取值范围 | 默认值 | 可用性 | 描述 | +|---|---|---|---|---|---| +| `regressionMethod` | choice | `linear` (Linear), `log` (Logarithmic), `exp` (Exponential), `pow` (Power), `quad` (Quadratic), `poly` (Polynomial) | `linear` | always | 回归拟合方法。 | +| `polyOrder` | number | 2 – 10 (step 1) | `3` | always | 回归拟合的多项式阶数。 | +| `independentYAxis` | toggle | on / off | `false` | conditional | 为分面使用独立的 y 轴比例尺。 | +| `logScale_x` | toggle | on / off | `false` | conditional | 在 x 轴上使用对数/symlog 比例尺。 | +| `logScale_y` | toggle | on / off | `false` | conditional | 在 y 轴上使用对数/symlog 比例尺。 | +| `includeZero_x` | toggle | on / off | `false` | conditional | 将 x 轴锚定在零。 | +| `includeZero_y` | toggle | on / off | `false` | conditional | 将 y 轴锚定在零。 | + +### ![](chart-icon-connected-scatter.svg) Connected Scatter Plot + +**编码通道:** `x`, `y`, `order`, `color`, `detail`, `column`, `row` + +| 参数 | 控件 | 取值范围 | 默认值 | 可用性 | 描述 | +|---|---|---|---|---|---| +| `independentYAxis` | toggle | on / off | `false` | conditional | 为分面使用独立的 y 轴比例尺。 | +| `logScale_x` | toggle | on / off | `false` | conditional | 在 x 轴上使用对数/symlog 比例尺。 | +| `logScale_y` | toggle | on / off | `false` | conditional | 在 y 轴上使用对数/symlog 比例尺。 | +| `includeZero_x` | toggle | on / off | `false` | conditional | 将 x 轴锚定在零。 | +| `includeZero_y` | toggle | on / off | `false` | conditional | 将 y 轴锚定在零。 | + +### ![](chart-icon-dot-plot-horizontal.svg) Ranged Dot Plot + +**编码通道:** `x`, `y`, `color` + +| 参数 | 控件 | 取值范围 | 默认值 | 可用性 | 描述 | +|---|---|---|---|---|---| +| `logScale_x` | toggle | on / off | `false` | conditional | 在 x 轴上使用对数/symlog 比例尺。 | +| `logScale_y` | toggle | on / off | `false` | conditional | 在 y 轴上使用对数/symlog 比例尺。 | +| `includeZero_x` | toggle | on / off | `false` | conditional | 将 x 轴锚定在零。 | +| `includeZero_y` | toggle | on / off | `false` | conditional | 将 y 轴锚定在零。 | + +### ![](chart-icon-strip-plot.svg) Strip Plot + +**编码通道:** `x`, `y`, `color`, `size`, `column`, `row` + +| 参数 | 控件 | 取值范围 | 默认值 | 可用性 | 描述 | +|---|---|---|---|---|---| +| `stepWidth` | number | 10 – 100 (step 5) | `20` | always | 抖动散布宽度。 | +| `pointSize` | number | 0 – 150 (step 5) | `0` | always | 点或标记大小。 | +| `opacity` | number | 0 – 1 (step 0.1) | `0` | always | 标记不透明度。 | +| `independentYAxis` | toggle | on / off | `false` | conditional | 为分面使用独立的 y 轴比例尺。 | +| `logScale_x` | toggle | on / off | `false` | conditional | 在 x 轴上使用对数/symlog 比例尺。 | +| `logScale_y` | toggle | on / off | `false` | conditional | 在 y 轴上使用对数/symlog 比例尺。 | +| `includeZero_x` | toggle | on / off | `false` | conditional | 将 x 轴锚定在零。 | +| `includeZero_y` | toggle | on / off | `false` | conditional | 将 y 轴锚定在零。 | + +## 条形图 + +### ![](chart-icon-column.svg) Bar Chart + +**编码通道:** `x`, `y`, `color`, `group`, `opacity`, `column`, `row` + +| 参数 | 控件 | 取值范围 | 默认值 | 可用性 | 描述 | +|---|---|---|---|---|---| +| `cornerRadius` | number | 0 – 15 (step 1) | `0` | always | 支持的标记的圆角半径。 | +| `independentYAxis` | toggle | on / off | `false` | conditional | 为分面使用独立的 y 轴比例尺。 | +| `xAxisType` | choice | `temporal` (Temporal), `nominal` (Discrete) | — | conditional | 将 x 轴解释为连续时间比例尺或离散波段。 | +| `yAxisType` | choice | `temporal` (Temporal), `nominal` (Discrete) | — | conditional | 将 y 轴解释为连续时间比例尺或离散波段。 | + +### ![](chart-icon-column-grouped.svg) Grouped Bar Chart + +**编码通道:** `x`, `y`, `group`, `column`, `row` + +| 参数 | 控件 | 取值范围 | 默认值 | 可用性 | 描述 | +|---|---|---|---|---|---| +| `dodge` | choice | `auto` (Auto), `local` (Local (compact)), `global` (Global (aligned)) | `auto` | conditional | Dodge | +| `independentYAxis` | toggle | on / off | `false` | conditional | 为分面使用独立的 y 轴比例尺。 | + +### ![](chart-icon-column-stacked.svg) Stacked Bar Chart + +**编码通道:** `x`, `y`, `color`, `column`, `row` + +| 参数 | 控件 | 取值范围 | 默认值 | 可用性 | 描述 | +|---|---|---|---|---|---| +| `stackMode` | choice | Stacked (default) _(default)_, `normalize` (Normalize (100%)), `center` (Center), `layered` (Layered (overlap)) | — | conditional | 重叠系列的堆叠策略。 | +| `independentYAxis` | toggle | on / off | `false` | conditional | 为分面使用独立的 y 轴比例尺。 | + +### ![](chart-icon-lollipop.svg) Lollipop Chart + +**编码通道:** `x`, `y`, `color`, `column`, `row` + +| 参数 | 控件 | 取值范围 | 默认值 | 可用性 | 描述 | +|---|---|---|---|---|---| +| `dotSize` | number | 20 – 300 (step 10) | `80` | always | 圆点标记的大小。 | +| `independentYAxis` | toggle | on / off | `false` | conditional | 为分面使用独立的 y 轴比例尺。 | +| `xAxisType` | choice | `temporal` (Temporal), `nominal` (Discrete) | — | conditional | 将 x 轴解释为连续时间比例尺或离散波段。 | +| `yAxisType` | choice | `temporal` (Temporal), `nominal` (Discrete) | — | conditional | 将 y 轴解释为连续时间比例尺或离散波段。 | + +### ![](chart-icon-waterfall.svg) Waterfall Chart + +**编码通道:** `x`, `y`, `color`, `column`, `row` + +| 参数 | 控件 | 取值范围 | 默认值 | 可用性 | 描述 | +|---|---|---|---|---|---| +| `cornerRadius` | number | 0 – 8 (step 1) | `0` | always | 支持的标记的圆角半径。 | +| `totals` | choice | `auto` (Auto), `none` (None), `first` (First), `last` (Last), `both` (Both) | `auto` | conditional | 合计 | +| `showTextLabels` | toggle | on / off | `false` | always | 在标记上渲染数值标签。 | +| `independentYAxis` | toggle | on / off | `false` | conditional | 为分面使用独立的 y 轴比例尺。 | + +### ![](chart-icon-gantt.svg) Gantt Chart + +**编码通道:** `y`, `x`, `x2`, `color`, `detail`, `column`, `row` + +| 参数 | 控件 | 取值范围 | 默认值 | 可用性 | 描述 | +|---|---|---|---|---|---| +| `independentYAxis` | toggle | on / off | `false` | conditional | 为分面使用独立的 y 轴比例尺。 | +| `logScale_x` | toggle | on / off | `false` | conditional | 在 x 轴上使用对数/symlog 比例尺。 | +| `logScale_y` | toggle | on / off | `false` | conditional | 在 y 轴上使用对数/symlog 比例尺。 | +| `includeZero_x` | toggle | on / off | `false` | conditional | 将 x 轴锚定在零。 | +| `includeZero_y` | toggle | on / off | `false` | conditional | 将 y 轴锚定在零。 | + +### ![](chart-icon-bullet.svg) Bullet Chart + +**编码通道:** `y`, `x`, `goal`, `color`, `column`, `row` + +| 参数 | 控件 | 取值范围 | 默认值 | 可用性 | 描述 | +|---|---|---|---|---|---| +| `independentYAxis` | toggle | on / off | `false` | conditional | 为分面使用独立的 y 轴比例尺。 | + +## 分布 + +### ![](chart-icon-histogram.svg) Histogram + +**编码通道:** `x`, `color`, `column`, `row` + +| 参数 | 控件 | 取值范围 | 默认值 | 可用性 | 描述 | +|---|---|---|---|---|---| +| `binCount` | number | 5 – 50 (step 1) | `Auto` | always | 最大分箱上限;Auto 由后端选择。 | +| `independentYAxis` | toggle | on / off | `false` | conditional | 为分面使用独立的 y 轴比例尺。 | + +### ![](chart-icon-density.svg) Density Plot + +**编码通道:** `x`, `color`, `column`, `row` + +| 参数 | 控件 | 取值范围 | 默认值 | 可用性 | 描述 | +|---|---|---|---|---|---| +| `bandwidth` | number | 0.05 – 2 (step 0.05) | `0` | always | 核密度带宽(0 = 自动)。 | +| `independentYAxis` | toggle | on / off | `false` | conditional | 为分面使用独立的 y 轴比例尺。 | + +### ![](chart-icon-ecdf.svg) ECDF Plot + +**编码通道:** `x`, `color`, `detail`, `column`, `row` + +| 参数 | 控件 | 取值范围 | 默认值 | 可用性 | 描述 | +|---|---|---|---|---|---| +| `showPoints` | toggle | on / off | `false` | always | 在线上叠加点标记。 | +| `independentYAxis` | toggle | on / off | `false` | conditional | 为分面使用独立的 y 轴比例尺。 | +| `logScale_x` | toggle | on / off | `false` | conditional | 在 x 轴上使用对数/symlog 比例尺。 | +| `logScale_y` | toggle | on / off | `false` | conditional | 在 y 轴上使用对数/symlog 比例尺。 | +| `includeZero_x` | toggle | on / off | `false` | conditional | 将 x 轴锚定在零。 | +| `includeZero_y` | toggle | on / off | `false` | conditional | 将 y 轴锚定在零。 | + +### ![](chart-icon-violin.svg) Violin Plot + +**编码通道:** `x`, `y`, `color`, `row` + +| 参数 | 控件 | 取值范围 | 默认值 | 可用性 | 描述 | +|---|---|---|---|---|---| +| `bandwidth` | number | 0.05 – 2 (step 0.05) | `0` | always | 核密度带宽(0 = 自动)。 | +| `independentYAxis` | toggle | on / off | `false` | conditional | 为分面使用独立的 y 轴比例尺。 | + +### ![](chart-icon-box-plot.svg) Boxplot + +**编码通道:** `x`, `y`, `color`, `opacity`, `column`, `row` + +| 参数 | 控件 | 取值范围 | 默认值 | 可用性 | 描述 | +|---|---|---|---|---|---| +| `whiskerMethod` | choice | `iqr` (Tukey (1.5 × IQR)), `minmax` (Min–Max) | `iqr` | always | 须线 | +| `showOutliers` | toggle | on / off | `true` | conditional | 异常值 | +| `dodge` | choice | `auto` (Auto), `local` (Local (compact)), `global` (Global (aligned)) | `auto` | conditional | Dodge | +| `independentYAxis` | toggle | on / off | `false` | conditional | 为分面使用独立的 y 轴比例尺。 | +| `logScale_x` | toggle | on / off | `false` | conditional | 在 x 轴上使用对数/symlog 比例尺。 | +| `logScale_y` | toggle | on / off | `false` | conditional | 在 y 轴上使用对数/symlog 比例尺。 | +| `includeZero_x` | toggle | on / off | `false` | conditional | 将 x 轴锚定在零。 | +| `includeZero_y` | toggle | on / off | `false` | conditional | 将 y 轴锚定在零。 | + +### ![](chart-icon-pyramid.svg) Pyramid Chart + +**编码通道:** `x`, `y`, `color` + +_无模板专属参数。_ + +### ![](chart-icon-candlestick.svg) Candlestick Chart + +**编码通道:** `x`, `open`, `high`, `low`, `close`, `column`, `row` + +| 参数 | 控件 | 取值范围 | 默认值 | 可用性 | 描述 | +|---|---|---|---|---|---| +| `independentYAxis` | toggle | on / off | `false` | conditional | 为分面使用独立的 y 轴比例尺。 | +| `logScale_x` | toggle | on / off | `false` | conditional | 在 x 轴上使用对数/symlog 比例尺。 | +| `logScale_y` | toggle | on / off | `false` | conditional | 在 y 轴上使用对数/symlog 比例尺。 | +| `includeZero_x` | toggle | on / off | `false` | conditional | 将 x 轴锚定在零。 | +| `includeZero_y` | toggle | on / off | `false` | conditional | 将 y 轴锚定在零。 | + +## 折线与面积 + +### ![](chart-icon-line.svg) Line Chart + +**编码通道:** `x`, `y`, `color`, `strokeDash`, `detail`, `opacity`, `column`, `row` + +| 参数 | 控件 | 取值范围 | 默认值 | 可用性 | 描述 | +|---|---|---|---|---|---| +| `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 | 折线或面积插值方法。 | +| `showPoints` | toggle | on / off | `false` | always | 在线上叠加点标记。 | +| `independentYAxis` | toggle | on / off | `false` | conditional | 为分面使用独立的 y 轴比例尺。 | +| `logScale_x` | toggle | on / off | `false` | conditional | 在 x 轴上使用对数/symlog 比例尺。 | +| `logScale_y` | toggle | on / off | `false` | conditional | 在 y 轴上使用对数/symlog 比例尺。 | +| `includeZero_x` | toggle | on / off | `false` | conditional | 将 x 轴锚定在零。 | +| `includeZero_y` | toggle | on / off | `false` | conditional | 将 y 轴锚定在零。 | +| `xAxisType` | choice | `temporal` (Temporal), `nominal` (Discrete) | — | conditional | 将 x 轴解释为连续时间比例尺或离散波段。 | +| `yAxisType` | choice | `temporal` (Temporal), `nominal` (Discrete) | — | conditional | 将 y 轴解释为连续时间比例尺或离散波段。 | + +### Sparkline + +**编码通道:** `x`, `y`, `color`, `detail`, `row`, `column` + +| 参数 | 控件 | 取值范围 | 默认值 | 可用性 | 描述 | +|---|---|---|---|---|---| +| `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 | 折线或面积插值方法。 | +| `baseline` | choice | `mean` (Average), `zero` (Zero), `median` (Median), `none` (None) | `mean` | always | 参考线 | +| `trendWidth` | number | 80 – 600 (step 10) | `240` | always | 迷你图宽度 | +| `independentYAxis` | toggle | on / off | `false` | conditional | 为分面使用独立的 y 轴比例尺。 | +| `logScale_x` | toggle | on / off | `false` | conditional | 在 x 轴上使用对数/symlog 比例尺。 | +| `logScale_y` | toggle | on / off | `false` | conditional | 在 y 轴上使用对数/symlog 比例尺。 | +| `includeZero_x` | toggle | on / off | `false` | conditional | 将 x 轴锚定在零。 | +| `includeZero_y` | toggle | on / off | `false` | conditional | 将 y 轴锚定在零。 | + +### ![](chart-icon-bump.svg) Bump Chart + +**编码通道:** `x`, `y`, `color`, `detail`, `column`, `row` + +| 参数 | 控件 | 取值范围 | 默认值 | 可用性 | 描述 | +|---|---|---|---|---|---| +| `independentYAxis` | toggle | on / off | `false` | conditional | 为分面使用独立的 y 轴比例尺。 | +| `logScale_x` | toggle | on / off | `false` | conditional | 在 x 轴上使用对数/symlog 比例尺。 | +| `logScale_y` | toggle | on / off | `false` | conditional | 在 y 轴上使用对数/symlog 比例尺。 | +| `includeZero_x` | toggle | on / off | `false` | conditional | 将 x 轴锚定在零。 | +| `includeZero_y` | toggle | on / off | `false` | conditional | 将 y 轴锚定在零。 | + +### ![](chart-icon-slope.svg) Slope Chart + +**编码通道:** `x`, `y`, `color`, `detail`, `column`, `row` + +| 参数 | 控件 | 取值范围 | 默认值 | 可用性 | 描述 | +|---|---|---|---|---|---| +| `independentYAxis` | toggle | on / off | `false` | conditional | 为分面使用独立的 y 轴比例尺。 | +| `logScale_x` | toggle | on / off | `false` | conditional | 在 x 轴上使用对数/symlog 比例尺。 | +| `logScale_y` | toggle | on / off | `false` | conditional | 在 y 轴上使用对数/symlog 比例尺。 | +| `includeZero_x` | toggle | on / off | `false` | conditional | 将 x 轴锚定在零。 | +| `includeZero_y` | toggle | on / off | `false` | conditional | 将 y 轴锚定在零。 | + +### ![](chart-icon-area.svg) Area Chart + +**编码通道:** `x`, `y`, `color`, `opacity`, `column`, `row` + +| 参数 | 控件 | 取值范围 | 默认值 | 可用性 | 描述 | +|---|---|---|---|---|---| +| `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 | 折线或面积插值方法。 | +| `opacity` | number | 0.1 – 1 (step 0.1) | `0.7` | always | 标记不透明度。 | +| `stackMode` | choice | Stacked (default) _(default)_, `normalize` (Normalize (100%)), `center` (Center), `layered` (Layered (overlap)) | — | conditional | 重叠系列的堆叠策略。 | +| `independentYAxis` | toggle | on / off | `false` | conditional | 为分面使用独立的 y 轴比例尺。 | +| `xAxisType` | choice | `temporal` (Temporal), `nominal` (Discrete) | — | conditional | 将 x 轴解释为连续时间比例尺或离散波段。 | +| `yAxisType` | choice | `temporal` (Temporal), `nominal` (Discrete) | — | conditional | 将 y 轴解释为连续时间比例尺或离散波段。 | + +### ![](chart-icon-streamgraph.svg) Streamgraph + +**编码通道:** `x`, `y`, `color`, `column`, `row` + +| 参数 | 控件 | 取值范围 | 默认值 | 可用性 | 描述 | +|---|---|---|---|---|---| +| `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 | 折线或面积插值方法。 | +| `independentYAxis` | toggle | on / off | `false` | conditional | 为分面使用独立的 y 轴比例尺。 | + +### ![](chart-icon-range-area.svg) Range Area Chart + +**编码通道:** `x`, `y`, `y2`, `color`, `column`, `row` + +| 参数 | 控件 | 取值范围 | 默认值 | 可用性 | 描述 | +|---|---|---|---|---|---| +| `interpolate` | choice | Default (linear) _(default)_, `linear` (Linear), `monotone` (Monotone (smooth)), `step` (Step), `step-before` (Step Before), `step-after` (Step After), `basis` (Basis (smooth)) | — | always | 折线或面积插值方法。 | +| `opacity` | number | 0.1 – 1 (step 0.1) | `0.5` | always | 标记不透明度。 | +| `independentYAxis` | toggle | on / off | `false` | conditional | 为分面使用独立的 y 轴比例尺。 | + +## 圆形图 + +### ![](chart-icon-pie.svg) Pie Chart + +**编码通道:** `size`, `color`, `column`, `row` + +| 参数 | 控件 | 取值范围 | 默认值 | 可用性 | 描述 | +|---|---|---|---|---|---| +| `innerRadius` | number | 0 – 100 (step 5) | `0` | always | 内半径占外半径的百分比。 | +| `sortSlices` | choice | `none` (Data order), `descending` (Largest first), `ascending` (Smallest first) | `none` | always | 排序扇区 | +| `independentYAxis` | toggle | on / off | `false` | conditional | 为分面使用独立的 y 轴比例尺。 | + +### ![](chart-icon-rose.svg) Rose Chart + +**编码通道:** `x`, `y`, `color`, `column`, `row` + +| 参数 | 控件 | 取值范围 | 默认值 | 可用性 | 描述 | +|---|---|---|---|---|---| +| `padAngle` | number | 0 – 0.1 (step 0.005) | `0` | always | 径向段之间的角度间距。 | +| `alignment` | choice | `left` (Left (default)), `center` (Center) | — | always | 径向图的段对齐方式。 | +| `sortSlices` | choice | `none` (Data order), `descending` (Largest first), `ascending` (Smallest first) | `none` | always | 排序扇区 | +| `independentYAxis` | toggle | on / off | `false` | conditional | 为分面使用独立的 y 轴比例尺。 | + +### ![](chart-icon-radar.svg) Radar Chart + +**编码通道:** `x`, `y`, `color`, `column`, `row` + +| 参数 | 控件 | 取值范围 | 默认值 | 可用性 | 描述 | +|---|---|---|---|---|---| +| `filled` | toggle | on / off | `true` | always | 填充雷达图封闭区域。 | +| `fillOpacity` | number | 0 – 0.5 (step 0.1) | `0.15` | always | 面积或区域的填充不透明度。 | +| `strokeWidth` | number | 0.5 – 4 (step 0.5) | `1.5` | always | 线条描边宽度。 | +| `independentYAxis` | toggle | on / off | `false` | conditional | 为分面使用独立的 y 轴比例尺。 | +| `logScale_x` | toggle | on / off | `false` | conditional | 在 x 轴上使用对数/symlog 比例尺。 | +| `logScale_y` | toggle | on / off | `false` | conditional | 在 y 轴上使用对数/symlog 比例尺。 | +| `includeZero_x` | toggle | on / off | `false` | conditional | 将 x 轴锚定在零。 | +| `includeZero_y` | toggle | on / off | `false` | conditional | 将 y 轴锚定在零。 | + +## 表格与地图 + +### ![](chart-icon-heat-map.svg) Heatmap + +**编码通道:** `x`, `y`, `color`, `column`, `row` + +| 参数 | 控件 | 取值范围 | 默认值 | 可用性 | 描述 | +|---|---|---|---|---|---| +| `showTextLabels` | toggle | on / off | `false` | always | 在标记上渲染数值标签。 | +| `independentYAxis` | toggle | on / off | `false` | conditional | 为分面使用独立的 y 轴比例尺。 | +| `xAxisType` | choice | `temporal` (Temporal), `nominal` (Discrete) | — | conditional | 将 x 轴解释为连续时间比例尺或离散波段。 | +| `yAxisType` | choice | `temporal` (Temporal), `nominal` (Discrete) | — | conditional | 将 y 轴解释为连续时间比例尺或离散波段。 | + +### ![](chart-icon-bar-table.svg) Bar Table + +**编码通道:** `y`, `x`, `color`, `column`, `row` + +| 参数 | 控件 | 取值范围 | 默认值 | 可用性 | 描述 | +|---|---|---|---|---|---| +| `maxRows` | number | 5 – 100 (step 1) | `20` | always | 显示的最大表格行数。 | +| `showPercent` | toggle | on / off | `false` | conditional | 将每个值显示为占总数的百分比。 | +| `independentYAxis` | toggle | on / off | `false` | conditional | 为分面使用独立的 y 轴比例尺。 | + +### ![](chart-icon-kpi-card.svg) KPI Card + +**编码通道:** `metric`, `value`, `goal` + +| 参数 | 控件 | 取值范围 | 默认值 | 可用性 | 描述 | +|---|---|---|---|---|---| +| `layout` | choice | `horizontal` (Horizontal), `vertical` (Vertical), `grid` (Grid) | `grid` | always | 布局 | +| `style` | toggle | on / off | `true` | always | 卡片样式 | +| `behindThreshold` | number | 0 – 1 (step 0.05) | `0.5` | conditional | 落后阈值 | +| `logScale_x` | toggle | on / off | `false` | conditional | 在 x 轴上使用对数/symlog 比例尺。 | +| `logScale_y` | toggle | on / off | `false` | conditional | 在 y 轴上使用对数/symlog 比例尺。 | +| `includeZero_x` | toggle | on / off | `false` | conditional | 将 x 轴锚定在零。 | +| `includeZero_y` | toggle | on / off | `false` | conditional | 将 y 轴锚定在零。 | + +### ![](chart-icon-world-map.svg) Map + +**编码通道:** `longitude`, `latitude`, `color`, `size`, `opacity` + +| 参数 | 控件 | 取值范围 | 默认值 | 可用性 | 描述 | +|---|---|---|---|---|---| +| `region` | choice | `auto` (Auto-detect), `us` (United States), `world` (World) | `auto` | always | 区域 | +| `projection` | choice | `default` (Default), `mercator` (Mercator), `equalEarth` (Equal Earth), `orthographic` (Orthographic (Globe)), `stereographic` (Stereographic), `conicEqualArea` (Conic Equal Area), `conicEquidistant` (Conic Equidistant), `azimuthalEquidistant` (Azimuthal Equidistant), `mollweide` (Mollweide) | `default` | conditional | 投影 | +| `projectionCenter` | choice | Default _(default)_, `0,0` (World (Atlantic) [0, 0]), `150,0` (World (Pacific) [150, 0]), `105,35` (China [105, 35]), `-98,39` (USA [-98, 39]), `10,50` (Europe [10, 50]), `138,36` (Japan [138, 36]), `78,22` (India [78, 22]), `-52,-14` (Brazil [-52, -14]), `134,-25` (Australia [134, -25]), `100,60` (Russia [100, 60]), `20,0` (Africa [20, 0]), `45,28` (Middle East [45, 28]), `115,5` (Southeast Asia [115, 5]), `-60,-15` (South America [-60, -15]), `-100,45` (North America [-100, 45]), `-2,54` (UK [-2, 54]), `10,51` (Germany [10, 51]), `2,47` (France [2, 47]), `128,36` (Korea [128, 36]) | — | conditional | 中心 | +| `logScale_x` | toggle | on / off | `false` | conditional | 在 x 轴上使用对数/symlog 比例尺。 | +| `logScale_y` | toggle | on / off | `false` | conditional | 在 y 轴上使用对数/symlog 比例尺。 | +| `includeZero_x` | toggle | on / off | `false` | conditional | 将 x 轴锚定在零。 | +| `includeZero_y` | toggle | on / off | `false` | conditional | 将 y 轴锚定在零。 | + +### ![](chart-icon-us-map.svg) Choropleth + +**编码通道:** `id`, `color`, `detail` + +| 参数 | 控件 | 取值范围 | 默认值 | 可用性 | 描述 | +|---|---|---|---|---|---| +| `region` | choice | `auto` (Auto-detect), `us` (United States), `world` (World) | `auto` | always | 区域 | diff --git a/docs/zh-CN/tutorials/agent-workflows.md b/docs/zh-CN/tutorials/agent-workflows.md new file mode 100644 index 00000000..807e2822 --- /dev/null +++ b/docs/zh-CN/tutorials/agent-workflows.md @@ -0,0 +1,326 @@ +# 智能体工作流 + +本指南介绍一种 Data Formulator 风格的集成:自定义智能体产品中,智能体协助创建图表,但宿主负责数据执行、验证、状态、UI 控件、编译、渲染与审阅。 + +若你只想在聊天或 IDE 客户端中将 Flint 作为 MCP 工具连接,请从[配置 Flint MCP](/documentation/setup-flint-mcp) 开始。本页聚焦库式集成:即 [Data Formulator](https://github.com/microsoft/data-formulator) 等产品所用的模式——智能体可提议数据塑形与图表请求,而产品控制实际运行与存储内容。 + +## 核心思想 + +不要让智能体以 Vega-Lite、ECharts、Chart.js 或渲染器代码作为主要产物。让它编写 Flint `ChartAssemblyInput`: + +```jsonc +{ + "data": { "values": [/* rows, or bound by the host */] }, + "semantic_types": { + "month": "YearMonth", + "product": "Category", + "revenue": "Quantity" + }, + "chart_spec": { + "chartType": "Line Chart", + "encodings": { + "x": { "field": "month" }, + "y": { "field": "revenue" }, + "color": { "field": "product" } + } + } +} +``` + +这使图表请求小而可检查、可编辑。Flint 编译器推导低层图表决策:轴类型、零基线、时间解析、数字格式、颜色默认值、尺寸、布局与后端特定的 mark 细节。 + +将 Flint 视为智能体与渲染器之间的语义图表层: + +```text +user intent + data context + ↓ +agent proposes data preparation + ChartAssemblyInput + ↓ +host validates fields, schema, policy, and backend support + ↓ +Flint compiles to Vega-Lite / ECharts / Chart.js + ↓ +product renders, stores, edits, or asks the agent for a revision +``` + +## 产品职责 + +在自定义工作流中,智能体不应拥有整个可视化系统。为每一部分明确分工: + +| 层级 | 职责 | +|------|------| +| Agent | 理解用户请求、检查数据上下文、提议转换、选择语义类型,并起草或修订 `chart_spec`。 | +| Host product | 执行数据转换、绑定行、验证字段、执行策略、存储图表状态、暴露 UI 控件并选择后端。 | +| Flint | 将语义图表请求编译为带确定性设计默认值的后端原生图表规范。 | +| Renderer | 在浏览器、notebook、服务或导出流水线中绘制后端规范。 | + +这种拆分使工作流稳健。智能体在语义层工作,语言模型在此有用。产品保持对执行、状态、安全与用户体验的控制。Flint 处理不应写在 prompt 中的可视化规则。 + +## 将 Flint 输入存为图表状态 + +尽可能存储 Flint 输入,而非生成的后端 JSON。 + +- **DataSpec** 是 `data` 与 `semantic_types` 部分。它告诉 Flint 有哪些列及其含义。 +- **ChartSpec** 是 `chart_spec` 部分。它告诉 Flint 使用哪种图表模板以及字段如何映射到通道。 + +存储的输入通常足够小,可放入产品数据库、notebook 单元、仪表盘配置或对话状态对象。它也可编辑:你的 UI 可更改通道、图表类型、排序选项、尺寸或图表属性,而无需让智能体重新生成低层后端 JSON。 + +按需编译后端规范: + +```ts +import { assembleChartjs, assembleECharts, assembleVegaLite } from 'flint-chart'; + +const vegaLiteSpec = assembleVegaLite(input); +const echartsOption = assembleECharts(input); +const chartjsConfig = assembleChartjs(input); +``` + +只安装产品需要的渲染器依赖: + +```bash +npm install flint-chart +npm install vega vega-lite vega-embed # browser Vega-Lite rendering +npm install echarts # ECharts rendering +npm install chart.js # Chart.js rendering +``` + +Python 支持将使用相同的输入结构,但计划在后续版本发布,不属于首次公开发版。目前请在已发布工作流中使用 npm 包或 MCP 服务器。 + +## 为智能体提供 authoring skill + +chart-author skill 位于 +[agent-skills/flint-chart-author/SKILL.md](https://github.com/microsoft/flint-chart/blob/main/agent-skills/flint-chart-author/SKILL.md)。 +将其作为参考说明用于智能体 prompt、工具描述或检索上下文。 + +当智能体未安装库或无法调用 live catalog 工具时,该 skill 很重要。它包含确切的图表类型名称、支持的通道、图表属性、语义类型与数据绑定规则。 + +给智能体的持久指令是: + +- 为字段选择语义类型,例如 `YearMonth`、`Quantity`、`Category`、`Price`、`Profit` 或 `Percentage`; +- 按确切名称选择受支持的 `chartType`; +- 将真实字段绑定到受支持通道,如 `x`、`y`、`color`、`row`、`column`、`size` 或 `group`; +- 当请求的视图需要聚合、过滤、连接、透视、派生列或宽/长重塑时,在 Flint 之前转换数据; +- 返回 Flint 输入,而非后端原生 JSON,除非用户明确要求 post-Flint 后端定制。 + +## 演练:Data Formulator 风格创作 + +想象一个 Data Formulator 风格产品,拥有原始 `orders` 表。用户问: + +```text +Show monthly revenue by region. +``` + +原始表包含如下列: + +| Field | Example | 含义 | +|-------|---------|------| +| `order_date` | `2025-01-17` | 每笔订单的日期 | +| `region` | `West` | 销售地区 | +| `segment` | `Consumer` | 客户细分 | +| `sales` | `1240.50` | 订单收入 | +| `profit` | `310.20` | 订单的有符号利润 | + +用户并未要求对单笔订单作图。他们要求月度聚合,因此产品不应让智能体把聚合藏进 Vega-Lite transform。产品应要求两件事: + +1. 创建可直接作图表格的代码或声明式计划; +2. 针对该派生表的 Flint `ChartAssemblyInput`。 + +### 1. 向智能体发送紧凑上下文 + +宿主发送用户请求与数据概要。通常无需发送整个数据集。 + +```text +Use the Flint chart authoring skill. + +User request: Show monthly revenue by region. + +Current table: orders +Fields: +- order_date: Date, examples 2025-01-17, 2025-01-22 +- region: Category, examples West, East, Central, South +- segment: Category, examples Consumer, Corporate +- sales: Quantity, numeric, non-negative +- profit: Profit, numeric, signed + +Return one JSON object with: +- transform_code: Python pandas code that creates a chart-ready DataFrame named chart_df +- chart_input: a Flint ChartAssemblyInput for chart_df + +Do not write Vega-Lite, ECharts, Chart.js, or renderer code. +Use only fields produced by chart_df in chart_input. +Leave chart_input.data.values empty; the host will bind rows after executing the transform. +``` + +### 2. 让智能体提议数据塑形与 Flint 输入 + +好的响应将数据转换与图表请求分开: + +```jsonc +{ + "transform_code": "import pandas as pd\nchart_df = orders.copy()\nchart_df['month'] = pd.to_datetime(chart_df['order_date']).dt.to_period('M').astype(str)\nchart_df = chart_df.groupby(['month', 'region'], as_index=False).agg(revenue=('sales', 'sum'))", + "chart_input": { + "data": { "values": [] }, + "semantic_types": { + "month": "YearMonth", + "region": "Region", + "revenue": "Amount" + }, + "chart_spec": { + "chartType": "Line Chart", + "encodings": { + "x": { "field": "month" }, + "y": { "field": "revenue" }, + "color": { "field": "region" } + } + } + } +} +``` + +重要属性是拆分:智能体不会把聚合塞进后端 JSON,Flint 输入只引用派生表中会存在的字段。 + +### 3. 在宿主中执行并检查 + +产品在自有可信或沙箱计算路径中执行 `transform_code`,然后在渲染前检查 `chart_df`。例如: + +```text +month region revenue +2025-01 East 18420.50 +2025-01 West 21310.10 +2025-02 East 19770.00 +2025-02 West 22640.75 +``` + +此时宿主可拒绝结果:代码使用未知列、产生过多行、出现意外空值或未能通过策略检查。智能体提议操作;产品决定是否运行并保留。 + +### 4. 绑定行并用 Flint 编译 + +执行后,宿主用 `chart_df` 的行填充 `chart_input.data.values` 并编译图表。 + +```ts +import { assembleVegaLite } from 'flint-chart'; + +const chartInput = { + ...agentResult.chart_input, + data: { values: chartRows }, +}; + +const vegaLiteSpec = assembleVegaLite(chartInput); +``` + +同一份存储的 Flint 输入稍后也可编译到其他后端: + +```ts +import { assembleECharts } from 'flint-chart'; + +const echartsOption = assembleECharts(chartInput); +``` + +### 5. 围绕 Flint 状态构建 UI + +在 Data Formulator 风格 UI 中,产品可同时展示两种产物: + +- 派生数据视图(`chart_df`),供用户检查正在作图的表格; +- ChartSpec 控件,使用户可更改图表类型、通道、排序、尺寸或图表属性,而无需编辑后端 JSON。 + +若用户说「也按 segment 拆分」,宿主可请智能体修订,或暴露直接 UI 操作,将 `segment` 加入 `column`、`row` 或 `color`,取决于图表设计。无论哪种方式,修订都会更改 Flint 输入并重新编译。 + +### 6. 将后端微调放在下游 + +若产品需要后端特定的标注或交互,在 Flint 编译语义图表之后应用。将 Flint 输入作为规范状态;将 patch 后的 Vega-Lite 或 ECharts JSON 视为渲染时产物。 + +## 可复用 prompt 模板 + +对于产品集成,使用比自由聊天更严格的 prompt。请智能体返回系统可验证的机器可读对象。当数据视图已可直接作图时,只要求 Flint 输入: + +```text +Use the Flint chart authoring skill. + +Return only a valid ChartAssemblyInput JSON object. +The current data view has fields: month, product, revenue, profit. +Create a monthly revenue trend by product. +Infer semantic_types, use only fields that exist, and do not write Vega-Lite, +ECharts, Chart.js, or renderer code. +``` + +当数据尚未可直接作图时,要求转换代码加 Flint 输入: + +```text +Use the Flint chart authoring skill. + +From the orders table, create monthly revenue by region. +Return one JSON object with: +- transform_code: Python pandas code that creates a chart-ready DataFrame named chart_df +- chart_input: a Flint ChartAssemblyInput for the transformed rows + +The chart_input.data field should be empty or placeholder rows. The host will +execute the code, inspect the derived table, and bind data.values afterward. +``` + +系统随后可解析 JSON、验证、执行或拒绝转换、存储接受的 Flint 输入、编译、渲染预览,或将具体反馈发回智能体。 + +## 渲染或保存前验证 + +验证属于宿主,不应只在 prompt 中。接受智能体创作的图表前,请检查: + +- 每个编码字段存在于当前或派生数据视图中; +- `semantic_types` 使用 Flint 已注册标签,而非自造名称; +- `chartType` 对所选后端与产品界面允许; +- 编码通道被该图表类型支持; +- 必需通道存在; +- 本地策略允许请求的后端、数据大小、图表大小与文件访问模式; +- 派生数据塑形发生在 Flint 上游,而非通过自造的 Flint transform 属性。 + +若产品内部使用 MCP 服务器,`validate_chart` 可执行单独的服务端验证。若产品直接嵌入库,在 try/catch 中调用相关 assembler,并在自有审阅 UI 中展示警告或错误。 + +## 构建编辑循环 + +首张图表渲染后,修订保持语义化。请智能体或 UI 更改 Flint 输入,而非后端规范: + +| 用户意图 | 产品操作 | +|----------|----------| +| "Compare regions side by side" | 更改图表类型,或将 region 字段路由到 `group`、`color`、`column` 或 `row`,取决于图表族。 | +| "Show profit instead of revenue" | 在 `chart_spec.encodings` 中替换度量字段。 | +| "Use a small multiple view" | 当图表支持分面时,将分组字段移到 `column` 或 `row`。 | +| "Make it a donut" | 保持 `Pie Chart` 并设置 `chartProperties.innerRadius`。 | +| "Try ECharts" | 用 `assembleECharts` 重新编译同一份 Flint 输入。 | + +这使用户编辑廉价且确定。智能体对模糊意图、数据转换或图表设计建议有用。常规 UI 编辑可直接修改 ChartSpec。 + +## 仅在 Flint 之后做后端定制 + +部分产品需求是后端特定的:标注、精确轴样式、自定义 mark 装饰或渲染器特定交互。将这些更改放在 Flint 编译步骤之后。 + +推荐路径: + +1. 将 Flint 输入存为规范图表状态。 +2. 编译到目标后端。 +3. 应用最小的后端特定展示 patch。 +4. 渲染 patch 后的后端规范。 + +不要将 patch 后的 Vega-Lite、ECharts 或 Chart.js JSON 回灌 Flint。一旦编辑后端 JSON,它就不再是可移植的 Flint 状态。 + +## 实用的宿主循环 + +典型的自定义智能体实现如下: + +1. 收集用户请求与紧凑数据概要:列名、样本行、语义提示、基数、最小/最大值与已知单位。 +2. 请智能体返回 `ChartAssemblyInput`,或转换代码加 `ChartAssemblyInput`。 +3. 在产品拥有的沙箱或可信计算路径中执行转换。 +4. 从当前或派生表绑定 `data.values`。 +5. 验证字段、语义类型、图表类型、通道、后端支持、尺寸与策略。 +6. 将 Flint 输入存为规范图表状态。 +7. 编译到所选后端并渲染。 +8. 让 UI 控件或智能体修订 Flint 输入,然后从验证重复。 + +该循环使 Flint 成为自然语言图表意图与生产渲染之间的稳定契约。 + +## 下一步 + +- [入门指南](/documentation/getting-started) 用一张小图解释 DataSpec 与 ChartSpec 的结构。 +- [示例:数据故事](/documentation/data-story) 展示同一份源数据视图如何仅通过更改 ChartSpec 变成多种图表设计。 +- [配置 Flint MCP](/documentation/setup-flint-mcp) 说明如何在需要现成智能体工具面时暴露 Flint 为 MCP 服务器。 +- [Vega-Lite charts](/documentation/reference-vegalite)、 + [ECharts charts](/documentation/reference-echarts) 与 + [Chart.js charts](/documentation/reference-chartjs) 按后端列出支持的图表类型。 +- [Semantic Type](/documentation/semantic-types) 列出智能体可用于字段的标签。 diff --git a/docs/zh-CN/tutorials/chart-sizing.md b/docs/zh-CN/tutorials/chart-sizing.md new file mode 100644 index 00000000..92861914 --- /dev/null +++ b/docs/zh-CN/tutorials/chart-sizing.md @@ -0,0 +1,105 @@ +# 示例:自动布局 + +Flint 的自动布局有一个目标:让图表适配数据与容器,同时保持 mark 可读。算法使用压力模型与 banking,在数据更密集时调整画布尺寸、mark 间距、宽高比与分面布局。 + +主要权衡是在图表内部压缩元素,与将图表拉伸超出 `baseSize` 之间的取舍。拉伸受 `canvasSize` 或默认增长上限约束,因此图表在需要时获得更多空间,但不会无限增长。 + +先试四个 demo,每种展示一种布局压力: +[带状轴](#带状轴) 用于类别与分箱, +[连续轴](#连续轴) 用于密集点、时间序列与多条线, +[径向图表](#径向图表) 用于扇区与辐条,以及 +[面积布局](#面积布局) 用于紧凑的二维空间。 +demo 将增长上限保持在库默认值,以便控件聚焦数据密度、`baseSize` 与 `elasticity`。 + +## 带状轴 + +Bar、histogram、heatmap 与 boxplot 为每个项目分配一个槽位。在下方添加类别,观察图表变宽直至拉伸上限用尽。 + +```flint-playground +discrete +``` + +## 连续轴 + +Scatter、line 与 area 图表不为每行分配一个 band,但密集点与多系列仍会产生压力。在下方添加点或系列,观察轴更渐进地拉伸。 + +```flint-playground +continuous +``` + +## 径向图表 + +Pie、rose、radar 及类似闭环图表需要足够的周长容纳每个扇区或辐条。在下方添加扇区,观察当请求的画布过紧时图表如何增大半径。 + +```flint-playground +circumference +``` + +## 面积布局 + +Treemap 与其他填充式二维布局需要足够总面积使矩形保持可读。在下方添加叶子节点,观察画布按面积而非单轴扩展。 + +```flint-playground +area +``` + +## 什么会被调整尺寸 + +自动布局改变同一张图表周围的空间:轴跨度、band 步长、分面单元、径向空间、填充布局面积与整体绘图区可增大或缩小。数据、语义编码、颜色与格式保持不变。 + +## 布局模式 + +通过设置 `baseSize`、`canvasSize` 或两者选择模式: + +| 模式 | 设置 | 行为 | +|------|------|------| +| **Default auto layout** | neither | Flint 以 `400 × 320` 为目标,数据密集时可增长。 | +| **Target size** | `baseSize` | Flint 以你偏好的尺寸为目标,但可读性需要时仍可增长。 | +| **Fixed slot** | `canvasSize` | Flint 将图表适配该盒子且永不溢出。适用于有硬性尺寸的仪表盘与卡片。 | +| **Bounded growth** | both | Flint 从 `baseSize` 出发,仅在需要时增长,且不超过 `canvasSize`。 | + +完整公式与实现细节见 +[Auto Layout Algorithm](/documentation/layout-model)。 + +## 控制预算 + +探索时使用默认值。设置 `baseSize` 更改目标,仅当周围 UI 有硬性尺寸约束时再添加 `canvasSize` 上限: + +```json +{ + "chart_spec": { + "chartType": "Bar Chart", + "encodings": { + "x": { "field": "category" }, + "y": { "field": "value" } + }, + "baseSize": { "width": 480, "height": 320 }, + "canvasSize": { "width": 720, "height": 480 } + }, + "options": { + "elasticity": 0.45, + "minStep": 8 + } +} +``` + +此处图表目标为 `480 × 320`,可拉伸至 `720 × 480` 上限。 +去掉 `canvasSize` 可让图表增长至 `baseSize × maxStretch`,或降低 +`options.maxStretch` 以收紧固定仪表盘单元格的默认上限。 + +对于**固定槽位**,最简形式是单独设置 `canvasSize` —— Flint 将其视为图表填充且收缩适配、永不溢出的盒子: + +```json +{ + "chart_spec": { + "chartType": "Bar Chart", + "encodings": { + "x": { "field": "category" }, + "y": { "field": "value" } + }, + "canvasSize": { "width": 320, "height": 240 } + } +} +``` + +关于分面与这些控件背后的模型细节,请继续阅读 [Auto Layout Algorithm](/documentation/layout-model)。 diff --git a/docs/zh-CN/tutorials/data-story.md b/docs/zh-CN/tutorials/data-story.md new file mode 100644 index 00000000..541db87e --- /dev/null +++ b/docs/zh-CN/tutorials/data-story.md @@ -0,0 +1,199 @@ +# 示例:数据故事 + +简单的折线图足以理解 Flint 规范的结构。当问题变得更复杂时,才真正有趣起来。 + +本示例使用游戏市场数据集:一组游戏组合的月度活跃用户,按月份、标题、游戏类型和地区拆分。我们将用同一份源数据,以三种方式推进叙事: + +- **概览**:用户在哪里?各地区趋势有何不同? +- **变化**:是什么推动组合上升或下降? +- **构成**:最新组合按地区、类型和标题如何分解? + +先说清楚一点:Flint 是图表编译器,不是完整的 ETL 引擎。它期望你传入的行已经匹配你想绘制的视图。在本故事中,各视图均源自同一数据集,但部分图表使用的是已汇总的、可直接作图的表格。瀑布图有意保持简单:它使用按月的 `period` / `newUsers` 行,模板将首尾两个 period 视为起始/结束柱。 + +这仍是重点。一旦视图存在,图表请求就保持精简:DataSpec 说明视图字段的含义,ChartSpec 说明如何绘制。 + +## 从真实源数据开始 + +源表每个 `period × game × region` 一行,每个标题附带游戏类型: + +| period | game | gameType | region | newUsers | totalUsers | +|--------|------|----------|--------|----------|------------| +| 2025-01 | Starforge Tactics | PC / Client | N | 5997 | 10173 | +| 2025-01 | Starforge Tactics | PC / Client | E | 682 | 4475 | +| 2025-01 | Starforge Tactics | PC / Client | S | -886 | 1917 | +| 2025-01 | Starforge Tactics | PC / Client | W | -1619 | 605 | +| 2025-01 | Neon Drift 2049 | Console | N | 8195 | 14920 | +| ... | ... | ... | ... | ... | ... | + +DataSpec 为这些列命名并记录其含义: + +```json +{ + "data": { "values": [ /* period × game × gameType × region rows */ ] }, + "semantic_types": { + "period": "YearMonth", + "game": "Category", + "gameType": "Category", + "newUsers": "Profit", + "totalUsers": "Quantity", + "region": { "semanticType": "Category", "sortOrder": ["N", "E", "S", "W"] } + } +} +``` + +这些语义标签承载的不只是名称: + +- `YearMonth` 告诉 Flint 将 `period` 解析为时间并格式化月份刻度。 +- `Quantity` 为 `totalUsers` 提供数值轴。 +- `Profit` 将 `newUsers` 标记为有符号值,因此热力图可在零附近使用发散色标。 +- `region.sortOrder` 使地区分面按 N、E、S、W 阅读,而非原始表的任意顺序。 + +下方示例使用从该源数据派生的、可直接作图的视图: + +- 折线视图:按 `region × period × gameType` 对 `sum(totalUsers)` 汇总; +- 分组柱状视图:按 `period × gameType` 对 `sum(totalUsers)` 汇总; +- 瀑布视图:按 `period` 对 `sum(newUsers)` 汇总,首尾 period 自动视为起始/结束柱; +- 热力图视图:按 `game × period` 对 `sum(newUsers)` 汇总; +- 旭日图视图:最新月份的 `region × gameType × game` 行,以 `totalUsers` 作为大小。 + +AI 智能体、SQL 查询、notebook 或应用层可以准备这些视图。Flint 随后处理图表相关的编译:坐标轴、标记、颜色、布局与后端语法。 + +## 第一幕:概览 + +首先提出一个宽泛问题:用户在哪里? + +折线视图包含 `region`、`period`、`gameType` 和 `totalUsers`。分面折线图为每个地区一个面板,每种游戏类型一条线: + +```json +"chart_spec": { + "chartType": "Line Chart", + "encodings": { + "column": { "field": "region" }, + "x": { "field": "period" }, + "y": { "field": "totalUsers" }, + "color": { "field": "gameType" } + } +} +``` + +```flint-chart +{ "generator": "Omni: Line", "canvasSize": { "width": 360, "height": 520 } } +``` + +值得注意: + +- ChartSpec 指定 `column: region`;Flint 处理 small-multiple 布局。 +- 因 DataSpec 声明 `YearMonth`,`period` 保持为时间轴。 +- 图表扩展为可读的分面布局,而非将所有面板挤进 base size。 + +接下来将视图从地区趋势面板切换为月度对比。分组柱状视图包含 `period`、`gameType` 和 `totalUsers`: + +```json +"chart_spec": { + "chartType": "Grouped Bar Chart", + "encodings": { + "x": { "field": "period" }, + "y": { "field": "totalUsers" }, + "color": { "field": "gameType" }, + "group": { "field": "gameType" } + } +} +``` + +```flint-chart +{ "generator": "Omni: Grouped Bar", "canvasSize": { "width": 640, "height": 340 }, "options": { "maxStretch": 1 } } +``` + +这是第一个回报:只需更换模板和少量通道,图表设计就从分面折线变为分组柱状。源语义保持不变,尽管分组柱状视图以不同方式汇总。 + +## 第二幕:变化 + +概览说明用户在哪里。现在问组合是如何到达那里的。对于这个瀑布图,视图仅为每月一行:`period` 加上组合范围内汇总的月度 `newUsers`。无需显式的 start/end/type 列;模板将第一个 period 视为起始柱,最后一个 period 视为结束柱,中间月份为增量。 + +ChartSpec 只是映射: + +```json +"chart_spec": { + "chartType": "Waterfall Chart", + "encodings": { + "x": { "field": "period" }, + "y": { "field": "newUsers" } + } +} +``` + +```flint-chart +{ "generator": "Omni: Waterfall", "canvasSize": { "width": 640, "height": 360 }, "options": { "maxStretch": 1 } } +``` + +定制部分在模板中:连接线、起始/结束推断、增量柱,以及后端特定的瀑布语法。规范只命名 period 与度量。 + +要查看哪些游戏驱动了波动,使用另一个派生视图:按 `game × period` 对 `sum(newUsers)` 汇总。热力图将月份放在 x、游戏放在 y,净用户数映射到颜色: + +```json +"chart_spec": { + "chartType": "Heatmap", + "encodings": { + "x": { "field": "period" }, + "y": { "field": "game" }, + "color": { "field": "newUsers" } + } +} +``` + +```flint-chart +{ "generator": "Omni: Heatmap", "canvasSize": { "width": 640, "height": 460 }, "options": { "maxStretch": 1 } } +``` + +这里语义类型做了简单图表中容易忽略的工作。因 `newUsers` 被当作 `Profit` 处理,Flint 知道零有意义,可使用红蓝发散色标,而非通用顺序渐变。 + +## 第三幕:构成 + +最后问最新组合由什么构成。 + +旭日图视图过滤到最新月份,保留 `region`、`gameType`、`game` 和 `totalUsers`。ChartSpec 描述层次结构:地区,再游戏类型,再单个游戏,以 total users 作为大小。 + +```json +"chart_spec": { + "chartType": "Sunburst Chart", + "encodings": { + "color": { "field": "region" }, + "group": { "field": "gameType" }, + "detail": { "field": "game" }, + "size": { "field": "totalUsers" } + } +} +``` + +```flint-chart +echarts +{ "generator": "Omni: Sunburst", "canvasSize": { "width": 460, "height": 460 } } +``` + +此图也演示了后端切换。Vega-Lite 没有原生 sunburst 图元,因此渲染使用 ECharts。输入仍读起来像 Flint:字段、通道、语义。因设计需要 ECharts 原生图表类型,后端随之改变。 + +## 本示例说明了什么 + +同一份源数据集经过五个可直接作图的视图与五种图表设计: + +- 分面折线图展示地区趋势; +- 分组柱状图进行月度对比; +- 瀑布图展示组合变化; +- 热力图展示游戏×月份变动; +- 旭日图展示层次构成。 + +三项 Flint 特性承担了大部分图表工作: + +- **语义类型驱动低层设置。** 时间解析、定量轴、发散颜色与有序分面来自 DataSpec。 +- **自动布局保持复杂视图可读。** 分面、分组柱、密集热力图与径向图可在可用画布内扩展或缩放。 +- **图表设计与后端切换成本低。** 视图具备正确字段后,ChartSpec 只需改几行,最终 assembler 在图表类型受支持时可指向 Vega-Lite、ECharts 或 Chart.js。 + +这就是示例背后的现实承诺:准备正确视图,标注字段含义,然后更换图表设计而无需手写后端规范。 + +## 下一步 + +- [入门指南](/documentation/getting-started) 用一张小图介绍 DataSpec 与 ChartSpec。 +- [Gallery](/gallery) 展示每种图表模板与后端组合。 +- [Semantic Type](/documentation/semantic-types) 说明语义标签如何驱动时间解析、颜色与聚合行为等默认值。 +- [Auto Layout Algorithm](/documentation/layout-model) 说明 Flint 如何为密集、分面与层次视图定尺寸。 +- [在线编辑器](/editor) 可实时编辑 Flint 规范。 diff --git a/docs/zh-CN/tutorials/getting-started.md b/docs/zh-CN/tutorials/getting-started.md new file mode 100644 index 00000000..935c26c1 --- /dev/null +++ b/docs/zh-CN/tutorials/getting-started.md @@ -0,0 +1,150 @@ +# 入门指南 + +Flint 从一个简单想法出发:先描述**你的数据代表什么**,再说明**你想要什么样的图**。Flint 会将其编译为可直接渲染的图表规范,目标后端为 Vega-Lite、ECharts 或 Chart.js。 + +本页刻意保持第一次接触时的体量较小。你将安装包、查看一份完整的 Flint 规范,并将其编译成图表。 + +## 安装 + +### JavaScript / TypeScript + +安装 Flint: + +```bash +npm install flint-chart +``` + +若要在浏览器中渲染 Vega-Lite 图表,还需安装渲染栈: + +```bash +npm install vega vega-lite vega-embed +``` + +### Python 规划中 + +Python 包计划在后续版本发布,不包含在首次公开发版中。目前请使用 JavaScript/TypeScript 包或 MCP 服务器;贡献者仍可从源码使用 Python 移植版。 + +## 你的第一份 Flint 规范 + +以下是一份小型月度注册量图表的完整输入: + +```json +{ + "data": { + "values": [ + { "month": "2024-01", "signups": 120 }, + { "month": "2024-02", "signups": 146 }, + { "month": "2024-03", "signups": 168 }, + { "month": "2024-04", "signups": 164 }, + { "month": "2024-05", "signups": 181 } + ] + }, + "semantic_types": { + "month": "YearMonth", + "signups": "Quantity" + }, + "chart_spec": { + "chartType": "Line Chart", + "encodings": { + "x": { "field": "month" }, + "y": { "field": "signups" } + }, + "baseSize": { "width": 420, "height": 280 } + } +} +``` + +```flint-chart +{ + "data": { + "values": [ + { "month": "2024-01", "signups": 120 }, + { "month": "2024-02", "signups": 146 }, + { "month": "2024-03", "signups": 168 }, + { "month": "2024-04", "signups": 164 }, + { "month": "2024-05", "signups": 181 } + ] + }, + "semantic_types": { + "month": "YearMonth", + "signups": "Quantity" + }, + "chart_spec": { + "chartType": "Line Chart", + "encodings": { + "x": { "field": "month" }, + "y": { "field": "signups" } + }, + "baseSize": { "width": 420, "height": 280 } + } +} +``` + +可以将其理解为两部分: + +- **DataSpec**:`data` 与 `semantic_types` 部分。表格数据在这里,每一列也会获得语义含义。`month` 为 `YearMonth`,因此 Flint 会将 `2024-01` 这类字符串当作日期处理。`signups` 为 `Quantity`,因此 Flint 会为其分配数值轴。若你面对的是凌乱的 CSV、数据库 schema 或自然语言请求,AI 智能体可以替你起草这部分;参见[智能体工作流](/documentation/agent-workflows)。 +- **ChartSpec**:`chart_spec` 部分。这是对图表外观的请求:使用 `Line Chart` 模板,将 `month` 放在 x 轴,将 `signups` 放在 y 轴。 + +这就是核心工作流。DataSpec 说明数据*是什么*。ChartSpec 说明你想*如何查看它*。将 JSON 粘贴到[在线编辑器](/editor)中即可实时编辑。 + +## 编译 + +在 JavaScript 或 TypeScript 中,将相同输入传给 assembler: + +```ts +import { assembleVegaLite } from 'flint-chart'; + +const input = { + data: { + values: [ + { month: '2024-01', signups: 120 }, + { month: '2024-02', signups: 146 }, + { month: '2024-03', signups: 168 }, + { month: '2024-04', signups: 164 }, + { month: '2024-05', signups: 181 }, + ], + }, + semantic_types: { + month: 'YearMonth', + signups: 'Quantity', + }, + chart_spec: { + chartType: 'Line Chart', + encodings: { + x: { field: 'month' }, + y: { field: 'signups' }, + }, + baseSize: { width: 420, height: 280 }, + }, +}; + +const spec = assembleVegaLite(input); +``` + +Flint 返回一份普通的 Vega-Lite 规范。使用 Vega-Embed 渲染: + +```ts +import embed from 'vega-embed'; + +await embed('#chart', spec); +``` + +同一份 Flint 输入在 JavaScript 中也可指向其他后端: + +```ts +import { assembleChartjs, assembleECharts } from 'flint-chart'; + +const chartjsConfig = assembleChartjs(input); +const echartsOption = assembleECharts(input); +``` + +Python 支持将使用相同的输入结构,计划在后续版本发布。 + +## 接下来读什么 + +- [示例:数据故事](/documentation/data-story) 说明这种拆分为何重要:同一份 DataSpec 只需修改 ChartSpec 即可生成五种不同图表。 +- [配置 Flint MCP](/documentation/setup-flint-mcp) 说明如何在聊天或 IDE 中让智能体渲染图表时连接 MCP 服务器。 +- [智能体工作流](/documentation/agent-workflows) 说明如何将 Flint 的图表契约嵌入自定义智能体或产品工作流。 +- [Semantic Type](/documentation/semantic-types) 解释 Flint 理解的语义标签,例如 `YearMonth`、`Quantity`、`Category` 和 `Profit`。 +- [Gallery](/gallery) 列出 Vega-Lite、ECharts 与 Chart.js 上可用的图表模板。 +- [概览](/documentation/overview) 在你准备好深入了解完整模型时提供架构说明。 diff --git a/docs/zh-CN/tutorials/setup-flint-mcp.md b/docs/zh-CN/tutorials/setup-flint-mcp.md new file mode 100644 index 00000000..0507a1c5 --- /dev/null +++ b/docs/zh-CN/tutorials/setup-flint-mcp.md @@ -0,0 +1,198 @@ +# 配置 Flint MCP + +本页说明如何在 MCP 客户端中运行 `flint-chart-mcp`,以及连接后服务器暴露的内容。它比简短的 MCP 概览页更详细,但仍从基本配置路径开始。 + +若你希望 VS Code、Claude Desktop、Cursor 或其他 MCP 客户端中的智能体创建、验证、预览或渲染 Flint 图表,请使用本页。对于直接嵌入库的自定义智能体与产品集成,参见[智能体工作流](/documentation/agent-workflows)。 + +## MCP 服务器提供什么 + +MCP 服务器是 Flint 面向智能体的执行端。智能体编写一份语义化的 `ChartAssemblyInput`,服务器在本地编译、验证、渲染或打开该图表。 + +| Tool | 用途 | +|------|------| +| `create_chart_view` | 当宿主支持 MCP Apps 时的首选默认:打开带实时 SVG 预览与图表选项的交互式图表视图。 | +| `validate_chart` | 检查 Flint 输入是否有效,并查看警告、错误与计算尺寸。 | +| `render_chart` | 需要产物或宿主无 MCP App UI 时,在本地渲染静态 PNG 或 SVG。 | +| `compile_chart` | 返回后端原生的 Vega-Lite、ECharts 或 Chart.js JSON。 | +| `list_chart_types` | 查看支持的图表类型与编码通道。 | + +| Resource or prompt | 用途 | +|--------------------|------| +| `flint://agent-skill` | 加载捆绑的 chart-author 说明。 | +| `flint://chart-types` | 浏览支持的图表目录。 | +| `ui://flint-chart/chart-view.html` | MCP App 宿主中 `create_chart_view` 使用的捆绑 UI 资源。 | +| `author_flint_chart` | 从嵌入 chart-author skill 的 prompt 开始。 | + +为获得最佳效果,请让客户端加载 `flint://agent-skill`,或运行 `author_flint_chart` 提示,再让智能体调用图表工具。该 skill 会教智能体有效的 `chartType` 名称、字段到通道的映射、语义类型、数据绑定规则,以及何时使用各渲染工具。 + +## 要求 + +你需要: + +- 能运行 stdio 服务器的 MCP 客户端; +- 该客户端可用的 Node.js 与 npm; +- 图表数据直接嵌入工具调用,或从主机上的本地文件读取。 + +服务器在主机上进程内渲染。内联行与本地文件保持本地;服务器不会将数据上传到远程渲染服务。 + +## 使用 `npx` 运行 + +多数客户端可通过 `npx` 运行已发布包,无需全局安装: + +```bash +npx -y flint-chart-mcp +``` + +该命令启动 stdio MCP 服务器。实践中,你通常将其写入客户端的 MCP 配置,而非手动运行。 + +## 配置 VS Code + +在 VS Code 中,于 `.vscode/mcp.json` 添加服务器条目: + +```jsonc +{ + "servers": { + "flint": { + "type": "stdio", + "command": "npx", + "args": ["-y", "flint-chart-mcp"] + } + } +} +``` + +若智能体应通过 `data.url` 对本地 `.csv`、`.tsv` 或 `.json` 文件作图,默认即可。若要在不可信部署中加强限制,使用 `--disable-file-reference` 完全拒绝本地文件引用(智能体须通过 `data.values` 内联传入行): + +```jsonc +{ + "servers": { + "flint": { + "type": "stdio", + "command": "npx", + "args": ["-y", "flint-chart-mcp", "--disable-file-reference"] + } + } +} +``` + +修改服务器代码或配置后,请在客户端重启 MCP 服务器。有用的冒烟测试是让智能体用 `list_chart_types` 列出 Flint 图表类型。 + +## 配置 Claude Desktop 或 Cursor + +许多 MCP 客户端使用 `mcpServers` 对象: + +```jsonc +{ + "mcpServers": { + "flint": { + "command": "npx", + "args": ["-y", "flint-chart-mcp"] + } + } +} +``` + +要完全禁用本地文件读取(仅内联 `data.values`): + +```jsonc +{ + "mcpServers": { + "flint": { + "command": "npx", + "args": ["-y", "flint-chart-mcp", "--disable-file-reference"] + } + } +} +``` + +## 从本仓库运行 + +开发 Flint 本身时,构建各包并将客户端指向本地 CLI。MCP 包依赖 `flint-chart`,因此需同时构建两者(根目录 `build` 脚本先构建 `flint-js`,再构建 `flint-mcp`): + +```bash +npm install +npm run build +``` + +若库已构建,仅需重建 MCP 包时,运行 `npm run build:mcp`。 + +VS Code 本地源码配置: + +```jsonc +{ + "servers": { + "flint": { + "type": "stdio", + "command": "node", + "args": [ + "${workspaceFolder}/packages/flint-mcp/dist/cli.js" + ] + } + } +} +``` + +修改服务器代码、渲染代码或捆绑的 MCP App UI 后,请重建并重启 MCP 服务器。 + +## 数据访问 + +MCP 工具调用可通过两种方式绑定数据: + +- **内嵌行:** 在工具调用中直接传入 `data: { values: [...] }`。这是小型或已准备表格的最简路径。 +- **本地文件引用:** 对主机上的 `.json`、`.csv` 或 `.tsv` 文件传入 `data: { url: "..." }`。 + +默认情况下,服务器信任主机并读取智能体引用的任意本地文件(相对路径相对于工作目录解析);智能体本也可内联相同行。远程 URL 永远不会被获取。 + +对于不可信部署,使用 `--disable-file-reference` 完全拒绝本地文件引用。智能体须通过 `data.values` 内联传入行: + +```bash +npx -y flint-chart-mcp --disable-file-reference +FLINT_MCP_DISABLE_FILE_REFERENCE=1 npx -y flint-chart-mcp +``` + +若图表请求需要聚合、过滤、连接、透视、派生列或长表重塑,请让智能体在调用 Flint 前准备可直接作图的表格。Flint 编译图表;它不是通用数据整理引擎。 + +## 后端与渲染选项 + +服务器支持以下后端: + +- `vegalite` 用于语法式统计图表; +- `echarts` 用于更丰富的交互与层次图表类型; +- `chartjs` 用于熟悉的 canvas 图表。Chart.js 仅渲染 PNG。 + +可在启动时只暴露子集: + +```bash +npx -y flint-chart-mcp --backends vegalite,echarts +FLINT_MCP_BACKENDS=vegalite,echarts npx -y flint-chart-mcp +``` + +当用户希望在支持 MCP App 的主机中查看并迭代图表时,使用 `create_chart_view`。需要静态产物时使用 `render_chart`。当用户需要供其他渲染器或编辑器使用的后端原生 JSON 时,使用 `compile_chart`。 + +## 验证配置 + +在 MCP 客户端中,请智能体做简单验证: + +```text +Load flint://agent-skill or run the author_flint_chart prompt. +Then call list_chart_types for the vegalite backend and tell me whether Flint is connected. +``` + +然后尝试第一张图表: + +```text +Use Flint MCP to create a bar chart from these rows: +[{"region":"North","revenue":120},{"region":"South","revenue":90}] +Use region as Category and revenue as Quantity. +Open it with create_chart_view if this client supports MCP Apps; otherwise render an SVG. +``` + +若 `list_chart_types` 可用但本地文件图表失败,请检查文件路径是否正确,以及是否未设置 `--disable-file-reference`。若 `create_chart_view` 不可用,主机可能不支持 MCP Apps;请让智能体改用 `render_chart`。 + +## 下一步 + +- [智能体工作流](/documentation/agent-workflows) 说明如何将 Flint 的语义图表契约嵌入自定义智能体或智能体产品。 +- [入门指南](/documentation/getting-started) 用一张小图解释 `DataSpec` 与 `ChartSpec` 的结构。 +- [Vega-Lite charts](/documentation/reference-vegalite)、 + [ECharts charts](/documentation/reference-echarts) 与 + [Chart.js charts](/documentation/reference-chartjs) 按后端列出支持的图表类型。 diff --git a/package-lock.json b/package-lock.json index f9300bbf..86f3f63b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5122,6 +5122,15 @@ "node": ">=16.9.0" } }, + "node_modules/html-parse-stringify": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", + "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==", + "license": "MIT", + "dependencies": { + "void-elements": "3.1.0" + } + }, "node_modules/html-url-attributes": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz", @@ -5152,6 +5161,34 @@ "url": "https://opencollective.com/express" } }, + "node_modules/i18next": { + "version": "26.3.6", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-26.3.6.tgz", + "integrity": "sha512-Bu5Z2nAXgfVyM8xvW3jk9EKRIuX37PudsrBViThNFx7CR7aaYTpP01cxNB/E4c4UUzTDiAZRstEhsRfPOL/8xA==", + "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.2", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", @@ -5508,7 +5545,6 @@ "os": [ "android" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -5530,7 +5566,6 @@ "os": [ "darwin" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -5552,7 +5587,6 @@ "os": [ "darwin" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -5574,7 +5608,6 @@ "os": [ "freebsd" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -5596,7 +5629,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -5618,7 +5650,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -5640,7 +5671,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -5662,7 +5692,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -5684,7 +5713,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -5706,7 +5734,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -5728,7 +5755,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -7409,6 +7435,33 @@ "react": "^18.3.1" } }, + "node_modules/react-i18next": { + "version": "17.0.10", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-17.0.10.tgz", + "integrity": "sha512-XneHftyYA774MJkkccSkZ5oKrUpCnXIPmxio3wemqrVzCRLWiGXOMbIzObrer03fNDEnm8g8R5yYls4HcE+esg==", + "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://registry.npmjs.org/react-markdown/-/react-markdown-10.1.0.tgz", @@ -8421,7 +8474,7 @@ "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -8603,6 +8656,15 @@ "punycode": "^2.1.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "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://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -9331,6 +9393,15 @@ "node": ">=18" } }, + "node_modules/void-elements": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", + "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/w3c-keyname": { "version": "2.2.8", "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", @@ -9759,9 +9830,11 @@ "chart.js": "^4.5.1", "echarts": "^6.0.0", "flint-chart": "*", + "i18next": "^26.3.6", "katex": "^0.17.0", "react": "^18.3.1", "react-dom": "^18.3.1", + "react-i18next": "^17.0.10", "react-markdown": "^10.1.0", "react-router-dom": "^6.30.4", "react-syntax-highlighter": "^16.1.1", diff --git a/site/package.json b/site/package.json index 88110524..72d39abd 100644 --- a/site/package.json +++ b/site/package.json @@ -17,9 +17,11 @@ "chart.js": "^4.5.1", "echarts": "^6.0.0", "flint-chart": "*", + "i18next": "^26.3.6", "katex": "^0.17.0", "react": "^18.3.1", "react-dom": "^18.3.1", + "react-i18next": "^17.0.10", "react-markdown": "^10.1.0", "react-router-dom": "^6.30.4", "react-syntax-highlighter": "^16.1.1", diff --git a/site/src/components/ChartCodeModal.tsx b/site/src/components/ChartCodeModal.tsx index bd0dccdb..c52e4931 100644 --- a/site/src/components/ChartCodeModal.tsx +++ b/site/src/components/ChartCodeModal.tsx @@ -1,4 +1,5 @@ import { useEffect, useMemo, useState } from 'react'; +import { useTranslation } from 'react-i18next'; import type { TestCase } from 'flint-chart/test-data'; import { JsonCodeMirror } from './JsonCodeMirror'; import { ScaleToFit } from './ScaleToFit'; @@ -7,6 +8,7 @@ import { GalleryOptionsBar } from './GalleryOptionsBar'; import { testCaseToAssemblyInput } from '../shared/test-case-utils'; import { buildPanelModel } from '../shared/chart-options'; import { buildGalleryEditorHref } from '../shared/editor-payload'; +import { useLocale } from '../i18n/LocaleContext'; import { humanizeVariants } from '../shared/wall-title'; import { BACKEND_LABELS, BACKENDS } from '../shared/supported-backends'; import type { ChartEntry } from '../shared/chart-categories'; @@ -37,6 +39,8 @@ export function ChartCodeModal({ editorIndices?: number[]; onClose: () => void; }) { + const { t } = useTranslation(); + const { locale } = useLocale(); const [index, setIndex] = useState(() => Math.min(Math.max(initialIndex, 0), Math.max(tests.length - 1, 0)), ); @@ -173,7 +177,7 @@ export function ChartCodeModal({ @@ -236,7 +240,7 @@ export function ChartCodeModal({ <>
diff --git a/site/src/components/GalleryOptionsBar.tsx b/site/src/components/GalleryOptionsBar.tsx index 163ce452..38e88d21 100644 --- a/site/src/components/GalleryOptionsBar.tsx +++ b/site/src/components/GalleryOptionsBar.tsx @@ -11,6 +11,7 @@ * button. */ import type { CSSProperties } from 'react'; +import { useTranslation } from 'react-i18next'; import type { ChartOption } from 'flint-chart'; import { siteTheme } from '../shared/theme'; import type { ControlSpec, PanelModel, ResolvedAction } from '../shared/chart-options'; @@ -184,6 +185,7 @@ function PivotControl(props: { onSelect: (id: string | undefined) => void; }) { const { pivot, width, onSelect } = props; + const { t } = useTranslation(); const { ids, labels, index, length, label } = pivot; const go = (delta: number) => { const nextIndex = (index + delta + length) % length; @@ -220,7 +222,7 @@ function PivotControl(props: {
{label}
- {index + 1} / {length} -
diff --git a/site/src/components/MarkdownView.tsx b/site/src/components/MarkdownView.tsx index 0130c0b2..0f055129 100644 --- a/site/src/components/MarkdownView.tsx +++ b/site/src/components/MarkdownView.tsx @@ -9,6 +9,7 @@ import { Link, useLocation } from 'react-router-dom'; import { CodeBlock, PlainTextBlock, resolveCodeLanguage } from './CodeBlock'; import { SizingPlayground } from './SizingPlayground'; import { DocChart } from './DocChart'; +import { useLocale } from '../i18n/LocaleContext'; import { resolveMarkdownHref, resolveMarkdownImageSrc } from '../shared/load-docs'; import { DOC_SCROLL_TO_KEY, scrollToHeading } from '../shared/scroll-to-heading'; import { siteTheme } from '../shared/theme'; @@ -27,7 +28,8 @@ function slugifyHeading(text: string): string { return text .trim() .toLowerCase() - .replace(/[^\w\s-]/g, '') + // Keep letters/numbers across scripts (incl. CJK) so Chinese headings get stable ids. + .replace(/[^\p{L}\p{N}\s-]/gu, '') .replace(/\s+/g, '-') .replace(/-+/g, '-') .replace(/^-|-$/g, ''); @@ -46,6 +48,7 @@ export function MarkdownView({ scrollContainerRef?: RefObject; }) { const location = useLocation(); + const { locale, lp } = useLocale(); const scrollRoot = () => scrollContainerRef?.current ?? null; const components: Components = { @@ -67,7 +70,7 @@ export function MarkdownView({ ); } - const internal = href ? resolveMarkdownHref(href) : null; + const internal = href ? resolveMarkdownHref(href, locale) : null; if (internal) { const [to, hash = ''] = internal.split('#'); return ( @@ -89,8 +92,10 @@ export function MarkdownView({ ); } if (href?.startsWith('/') && !href.startsWith('//')) { + const [pathPart, hash = ''] = href.split('#'); + const to = lp(pathPart); return ( - + {children} ); diff --git a/site/src/components/SiteShell.tsx b/site/src/components/SiteShell.tsx index ec3367b7..9c386aee 100644 --- a/site/src/components/SiteShell.tsx +++ b/site/src/components/SiteShell.tsx @@ -1,6 +1,11 @@ import type { CSSProperties, ReactNode } from 'react'; import { useEffect, useState } from 'react'; -import { Link, useLocation } from 'react-router-dom'; +import { useTranslation } from 'react-i18next'; +import { useLocation } from 'react-router-dom'; +import { LocaleLink } from '../i18n/LocaleLink'; +import { useLocale } from '../i18n/LocaleContext'; +import { stripLocale } from '../i18n/paths'; +import type { Locale } from '../i18n/locales'; import { CONTENT_MAX_WIDTH, GITHUB_REPO, siteTheme } from '../shared/theme'; const GITHUB_REPO_API = 'https://api.github.com/repos/microsoft/flint-chart'; @@ -33,6 +38,8 @@ export function SiteShell({ children }: { children: ReactNode }) { export function SiteNavBar(_props: { flush?: boolean } = {}) { const { pathname } = useLocation(); + const logical = stripLocale(pathname); + const { t } = useTranslation(); return (
+
@@ -89,7 +93,7 @@ function NavLink({ to, active, children }: { to: string; active: boolean; childr const [hovered, setHovered] = useState(false); const underline = active || hovered; return ( - setHovered(true)} onMouseLeave={() => setHovered(false)} @@ -109,22 +113,14 @@ function NavLink({ to, active, children }: { to: string; active: boolean; childr }} > {children} - - ); -} - -function NavLinkExternal({ href, label }: { href: string; label: string }) { - return ( - - {label} - + ); } function BrandLink() { const [hovered, setHovered] = useState(false); return ( - setHovered(true)} onMouseLeave={() => setHovered(false)} @@ -135,14 +131,62 @@ function BrandLink() { }} > flint-chart - + + ); +} + +function LanguageSwitch() { + const { t } = useTranslation(); + const { locale, setLocale } = useLocale(); + const options: { id: Locale; labelKey: 'nav.langEn' | 'nav.langZh' }[] = [ + { id: 'en', labelKey: 'nav.langEn' }, + { id: 'zh-CN', labelKey: 'nav.langZh' }, + ]; + + return ( +
+ {options.map((opt, i) => { + const active = locale === opt.id; + return ( + + {i > 0 ? ( + + ) : null} + + + ); + })} +
); } function GitHubLink() { + const { t, i18n } = useTranslation(); const [hovered, setHovered] = useState(false); const [starCount, setStarCount] = useState(readCachedGitHubStars); const compactStarCount = starCount === null ? '' : formatCompactCount(starCount); + const countLabel = starCount === null ? '' : starCount.toLocaleString(i18n.language); useEffect(() => { let active = true; @@ -164,8 +208,16 @@ function GitHubLink() { href={GITHUB_REPO} target="_blank" rel="noreferrer" - aria-label={starCount === null ? 'GitHub repository' : `GitHub repository, ${starCount.toLocaleString()} stars`} - title={starCount === null ? 'View on GitHub' : `View on GitHub (${starCount.toLocaleString()} stars)`} + aria-label={ + starCount === null + ? t('nav.githubAria') + : t('nav.githubAriaStars', { count: countLabel }) + } + title={ + starCount === null + ? t('nav.githubTitle') + : t('nav.githubTitleStars', { count: countLabel }) + } onMouseEnter={() => setHovered(true)} onMouseLeave={() => setHovered(false)} style={{ @@ -178,7 +230,7 @@ function GitHubLink() { }} > - GitHub + {t('nav.github')}