// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. import { ChartTemplateDef, ChartPropertyDef, type InstantiateContext } from '../../core/types'; import { defaultBuildEncodings, setMarkProp } from './utils'; import { makeCartesianPivot } from '../../core/pivot'; export const interpolateConfigProperty: ChartPropertyDef = { key: "interpolate", label: "Curve", type: "discrete", options: [ { value: undefined, label: "Default (linear)" }, { value: "linear", label: "Linear" }, { value: "monotone", label: "Monotone (smooth)" }, { value: "step", label: "Step" }, { value: "step-before", label: "Step Before" }, { value: "step-after", label: "Step After" }, { value: "basis", label: "Basis (smooth)" }, { value: "cardinal", label: "Cardinal" }, { value: "catmull-rom", label: "Catmull-Rom" }, ], }; const showPointsProperty: ChartPropertyDef = { key: "showPoints", label: "Points", type: "binary", defaultValue: false, }; export function applyInterpolate(mark: any, config?: Record): any { if (!config?.interpolate) return mark; return setMarkProp(mark, 'interpolate', config.interpolate); } function applyShowPoints(mark: any, config?: Record): any { if (!config?.showPoints) return mark; return setMarkProp(mark, 'point', true); } function isContinuousColor(ctx: InstantiateContext): boolean { const color = ctx.resolvedEncodings.color; if (!color?.field) return false; const type = color.type ?? ctx.channelSemantics.color?.type; return type === 'quantitative' || type === 'temporal'; } /** * Vega-Lite splits a line into one segment per datum when color is quantitative, * so nothing visible connects. Mirror ECharts: a neutral line + colored points. */ function buildContinuousColorLayers( spec: any, resolvedEncodings: Record, chartProperties?: Record, ): void { const { color, column, row, x, y, strokeDash, detail, opacity, order, ...rest } = resolvedEncodings; const lineEncoding: Record = {}; for (const [ch, enc] of Object.entries({ x, y, strokeDash, detail, opacity, order, ...rest })) { if (enc && typeof enc === 'object' && Object.keys(enc).length > 0) { lineEncoding[ch] = enc; } } const pointEncoding: Record = {}; if (x) pointEncoding.x = x; if (y) pointEncoding.y = y; if (color) pointEncoding.color = color; if (detail) pointEncoding.detail = detail; if (opacity) pointEncoding.opacity = opacity; spec.layer = [ { mark: applyInterpolate({ type: 'line', color: '#cccccc' }, chartProperties), encoding: lineEncoding, }, { mark: { type: 'point', filled: true, size: 80 }, encoding: pointEncoding, }, ]; delete spec.mark; if (column || row) { if (!spec.encoding) spec.encoding = {}; if (column) spec.encoding.column = column; if (row) spec.encoding.row = row; } else { delete spec.encoding; } } export const lineChartDef: ChartTemplateDef = { chart: "Line Chart", template: { mark: "line", encoding: {} }, channels: ["x", "y", "color", "strokeDash", "detail", "opacity", "column", "row"], markCognitiveChannel: 'position', declareLayoutMode: () => ({ paramOverrides: { continuousMarkCrossSection: { x: 100, y: 20, seriesCountAxis: 'auto' }, facetAspectRatioResistance: 0.5 }, }), instantiate: (spec, ctx) => { if (isContinuousColor(ctx)) { buildContinuousColorLayers(spec, ctx.resolvedEncodings, ctx.chartProperties); return; } defaultBuildEncodings(spec, ctx.resolvedEncodings); spec.mark = applyInterpolate(spec.mark, ctx.chartProperties); spec.mark = applyShowPoints(spec.mark, ctx.chartProperties); }, properties: [interpolateConfigProperty, showPointsProperty], // No `transpose`: a line pins its domain to `x` (never a vertical line, for any // x type). `permute` excludes `x`, so only a genuine dual-measure line offers a // y↔color swap; the series dimension is explored via `shift` (facets/legend). pivot: makeCartesianPivot({ permute: [['y', 'color']], shift: ['color', 'group', 'column', 'row'] }), };