@opendata-ai/openchart-engine 6.26.0 → 6.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as _opendata_ai_openchart_core from '@opendata-ai/openchart-core';
2
- import { LegendLayout, ResolvedChrome, TooltipContent, A11yMetadata, ResolvedTheme, CompileOptions, ChartLayout, LayerSpec, CompileTableOptions, TableLayout, AnimationSpec, ResolvedAnimation, TileMapEncoding, TileMapPalette, LegendConfig, ThemeConfig, DarkMode, MarkType, MarkDef, DataRow, Encoding, ChromeText, Annotation, LabelConfig, ColumnConfig, GraphSpec, GraphEncoding, GraphLayoutConfig, NodeOverride, SankeyEncoding, SankeyNodeAlign, SankeyLinkColor, VizSpec, ScaleType, EncodingChannel, Rect, LayoutStrategy, Mark, BinTransform, CalculateTransform, ConditionalValueDef, FilterPredicate, TimeUnitTransform, Transform } from '@opendata-ai/openchart-core';
2
+ import { LegendLayout, ResolvedChrome, TooltipContent, A11yMetadata, ResolvedTheme, CompileOptions, ChartLayout, LayerSpec, CompileTableOptions, TableLayout, AnimationSpec, ResolvedAnimation, TileMapEncoding, TileMapPalette, LegendConfig, ThemeConfig, DarkMode, MarkType, MarkDef, DataRow, Encoding, ChromeText, Annotation, LabelConfig, Display, ColumnConfig, GraphSpec, GraphEncoding, GraphLayoutConfig, NodeOverride, SankeyEncoding, SankeyNodeAlign, SankeyLinkColor, VizSpec, ScaleType, EncodingChannel, Rect, LayoutStrategy, Mark, BinTransform, CalculateTransform, ConditionalValueDef, FilterPredicate, TimeUnitTransform, Transform } from '@opendata-ai/openchart-core';
3
3
  export { ChartLayout, ChartSpec, CompileOptions, CompileTableOptions, GraphLayout, GraphSpec, LayerSpec, SankeyLayout, SankeySpec, TableLayout, TableSpec, TileMapLayout, TileMapSpec, VizSpec } from '@opendata-ai/openchart-core';
4
4
  import { ScaleLinear, ScaleTime, ScaleLogarithmic, ScalePower, ScaleSymLog, ScaleBand, ScalePoint, ScaleOrdinal, ScaleQuantile, ScaleQuantize, ScaleThreshold } from 'd3-scale';
5
5
 
@@ -232,6 +232,34 @@ interface NormalizedChrome {
232
232
  byline?: ChromeText;
233
233
  footer?: ChromeText;
234
234
  }
235
+ /**
236
+ * Tracks which top-level fields the user explicitly set in their input spec.
237
+ *
238
+ * Built from the raw expandedSpec (post-breakpoint-merge, pre-normalize) so
239
+ * that "user wrote chrome.title" vs "user wrote nothing" is distinguishable
240
+ * after normalization fills in defaults.
241
+ *
242
+ * Used by sparkline display mode to decide whether to suppress chrome/axes/
243
+ * legend/etc. by default vs. respecting an explicit user opt-in.
244
+ */
245
+ interface UserExplicit {
246
+ /** True if user wrote `chrome` (any non-empty chrome). */
247
+ chrome: boolean;
248
+ /** True if user wrote `legend`. */
249
+ legend: boolean;
250
+ /** True if user wrote `encoding.x.axis`. */
251
+ xAxis: boolean;
252
+ /** True if user wrote `encoding.y.axis`. */
253
+ yAxis: boolean;
254
+ /** True if user wrote `labels`. */
255
+ labels: boolean;
256
+ /** True if user wrote `animation`. */
257
+ animation: boolean;
258
+ /** True if user wrote `watermark`. */
259
+ watermark: boolean;
260
+ /** True if user wrote `crosshair`. */
261
+ crosshair: boolean;
262
+ }
235
263
  /** A ChartSpec with all optional fields filled with sensible defaults. */
236
264
  interface NormalizedChartSpec {
237
265
  /** Resolved mark type string (extracted from spec.mark). */
@@ -255,6 +283,14 @@ interface NormalizedChartSpec {
255
283
  hiddenSeries: string[];
256
284
  /** Per-series visual style overrides. */
257
285
  seriesStyles: Record<string, _opendata_ai_openchart_core.SeriesStyle>;
286
+ /** Display mode controlling chrome/axes/legend stripping. Defaults to `'full'`. */
287
+ display: Display;
288
+ /**
289
+ * Which top-level fields the user explicitly set. Populated by compileChart
290
+ * from the raw expanded spec before normalization. NormalizeChartSpec runs
291
+ * with a default-empty descriptor; compileChart overwrites it post-normalize.
292
+ */
293
+ userExplicit: UserExplicit;
258
294
  }
259
295
  /** A TableSpec with all optional fields filled with sensible defaults. */
260
296
  interface NormalizedTableSpec {
package/dist/index.js CHANGED
@@ -5351,7 +5351,7 @@ function computeSingleArea(spec, scales, _chartArea) {
5351
5351
  fill: fillValue,
5352
5352
  fillOpacity,
5353
5353
  stroke: getRepresentativeColor4(isGradientDef3(fillValue) ? color2 : fillValue),
5354
- strokeWidth: 2,
5354
+ strokeWidth: spec.display === "sparkline" ? 1.25 : 2,
5355
5355
  seriesKey: seriesKey === "__default__" ? void 0 : seriesKey,
5356
5356
  data: validPoints.map((p) => p.row),
5357
5357
  dataPoints: validPoints.map((p) => ({ x: p.x, y: p.yTop, datum: p.row })),
@@ -5468,6 +5468,7 @@ function computeAreaMarks(spec, scales, chartArea) {
5468
5468
  // src/charts/line/compute.ts
5469
5469
  import { getRepresentativeColor as getRepresentativeColor5 } from "@opendata-ai/openchart-core";
5470
5470
  var DEFAULT_STROKE_WIDTH = 2.5;
5471
+ var SPARKLINE_STROKE_WIDTH = 1.25;
5471
5472
  var DEFAULT_POINT_RADIUS = 3;
5472
5473
  function computeLineMarks(spec, scales, _chartArea, _strategy) {
5473
5474
  const encoding = spec.encoding;
@@ -5534,7 +5535,7 @@ function computeLineMarks(spec, scales, _chartArea, _strategy) {
5534
5535
  points: allPoints,
5535
5536
  path: combinedPath,
5536
5537
  stroke: strokeColor,
5537
- strokeWidth: styleOverride?.strokeWidth ?? DEFAULT_STROKE_WIDTH,
5538
+ strokeWidth: styleOverride?.strokeWidth ?? (spec.display === "sparkline" ? SPARKLINE_STROKE_WIDTH : DEFAULT_STROKE_WIDTH),
5538
5539
  strokeDasharray,
5539
5540
  opacity: styleOverride?.opacity,
5540
5541
  seriesKey: seriesStyleKey,
@@ -6812,6 +6813,12 @@ function normalizeChartSpec(spec, warnings) {
6812
6813
  const encoding = inferEncodingTypes(spec.encoding, spec.data, warnings);
6813
6814
  const markType = resolveMarkType(spec.mark);
6814
6815
  const markDef = resolveMarkDef(spec.mark);
6816
+ const display = spec.display ?? "full";
6817
+ if (display === "sparkline" && markType !== "line" && markType !== "area" && markType !== "bar" && markType !== "point") {
6818
+ warnings.push(
6819
+ `[openchart] display: 'sparkline' works best with mark: 'line' | 'area' | 'bar' | 'point'. Got mark: '${markType}' \u2014 rendering may degrade.`
6820
+ );
6821
+ }
6815
6822
  return {
6816
6823
  markType,
6817
6824
  markDef,
@@ -6826,7 +6833,20 @@ function normalizeChartSpec(spec, warnings) {
6826
6833
  darkMode: spec.darkMode ?? "off",
6827
6834
  hiddenSeries: spec.hiddenSeries ?? [],
6828
6835
  seriesStyles: spec.seriesStyles ?? {},
6829
- watermark: spec.watermark ?? true
6836
+ watermark: spec.watermark ?? true,
6837
+ display,
6838
+ // Default empty userExplicit; compileChart overwrites this with the real
6839
+ // descriptor built from the raw expanded spec before normalize runs.
6840
+ userExplicit: {
6841
+ chrome: false,
6842
+ legend: false,
6843
+ xAxis: false,
6844
+ yAxis: false,
6845
+ labels: false,
6846
+ animation: false,
6847
+ watermark: false,
6848
+ crosshair: false
6849
+ }
6830
6850
  };
6831
6851
  }
6832
6852
  function normalizeTableSpec(spec, _warnings) {
@@ -8473,7 +8493,7 @@ function computeAxes(scales, chartArea, strategy, theme, measureText, dataContex
8473
8493
  };
8474
8494
  const { fontSize } = tickLabelStyle;
8475
8495
  const { fontWeight } = tickLabelStyle;
8476
- if (scales.x) {
8496
+ if (scales.x && !dataContext?.skipX) {
8477
8497
  const axisConfig = scales.x.channel.axis;
8478
8498
  const isContinuousX = scales.x.type !== "band" && scales.x.type !== "point" && scales.x.type !== "ordinal";
8479
8499
  const xTargetCount = isContinuousX ? targetTickCount(chartArea.width, xDensity, "x") : void 0;
@@ -8551,7 +8571,7 @@ function computeAxes(scales, chartArea, strategy, theme, measureText, dataContex
8551
8571
  labelFlush: axisConfig?.labelFlush
8552
8572
  };
8553
8573
  }
8554
- if (scales.y) {
8574
+ if (scales.y && !dataContext?.skipY) {
8555
8575
  const axisConfig = scales.y.channel.axis;
8556
8576
  const isContinuousY = scales.y.type !== "band" && scales.y.type !== "point" && scales.y.type !== "ordinal";
8557
8577
  const yTargetCount = isContinuousY ? targetTickCount(chartArea.height, yDensity, "y") : void 0;
@@ -8703,12 +8723,24 @@ function scalePadding(basePadding, width, height) {
8703
8723
  }
8704
8724
  var MIN_CHART_WIDTH = 60;
8705
8725
  var MIN_CHART_HEIGHT = 40;
8726
+ function getMinChartDims(display) {
8727
+ return display === "sparkline" ? { width: 30, height: 20 } : { width: MIN_CHART_WIDTH, height: MIN_CHART_HEIGHT };
8728
+ }
8729
+ function getSparklinePad(spec) {
8730
+ const strokeWidth = spec.markDef.strokeWidth ?? 2;
8731
+ return Math.max(strokeWidth / 2 + 1, 2);
8732
+ }
8706
8733
  function computeDimensions(spec, options, legendLayout, theme, strategy, watermark = true) {
8707
8734
  const { width, height } = options;
8708
8735
  const padding = scalePadding(theme.spacing.padding, width, height);
8709
8736
  const hPad = width < BREAKPOINT_COMPACT_MAX ? Math.max(Math.round(padding * HPAD_COMPACT_FRACTION), HPAD_COMPACT_MIN) : padding;
8710
8737
  const axisMargin = theme.spacing.axisMargin;
8711
- const chromeMode = strategy?.chromeMode ?? "full";
8738
+ const userExplicit = spec.userExplicit;
8739
+ const isSparkline = spec.display === "sparkline";
8740
+ let chromeMode = strategy?.chromeMode ?? "full";
8741
+ if (isSparkline && !userExplicit.chrome) {
8742
+ chromeMode = "hidden";
8743
+ }
8712
8744
  const chrome = computeChrome2(
8713
8745
  chromeToInput(spec.chrome),
8714
8746
  theme,
@@ -8718,6 +8750,35 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
8718
8750
  padding,
8719
8751
  watermark
8720
8752
  );
8753
+ if (isSparkline) {
8754
+ const total2 = { x: 0, y: 0, width, height };
8755
+ const sparkPad = getSparklinePad(spec);
8756
+ const xAxisSpace = userExplicit.xAxis ? 26 : 0;
8757
+ const yAxisSpace = userExplicit.yAxis ? 30 : 0;
8758
+ const margins2 = {
8759
+ top: chrome.topHeight + sparkPad,
8760
+ right: sparkPad,
8761
+ bottom: chrome.bottomHeight + sparkPad + xAxisSpace,
8762
+ left: sparkPad + yAxisSpace
8763
+ };
8764
+ if (userExplicit.legend && "entries" in legendLayout && legendLayout.entries.length > 0) {
8765
+ const gap = legendGap(width);
8766
+ if (legendLayout.position === "right" || legendLayout.position === "bottom-right") {
8767
+ margins2.right += legendLayout.bounds.width + 8;
8768
+ } else if (legendLayout.position === "top") {
8769
+ margins2.top += legendLayout.bounds.height + gap;
8770
+ } else if (legendLayout.position === "bottom") {
8771
+ margins2.bottom += legendLayout.bounds.height + gap;
8772
+ }
8773
+ }
8774
+ const chartArea2 = {
8775
+ x: margins2.left,
8776
+ y: margins2.top,
8777
+ width: Math.max(0, width - margins2.left - margins2.right),
8778
+ height: Math.max(0, height - margins2.top - margins2.bottom)
8779
+ };
8780
+ return { total: total2, chrome, chartArea: chartArea2, margins: margins2, theme };
8781
+ }
8721
8782
  const total = { x: 0, y: 0, width, height };
8722
8783
  const isRadial = spec.markType === "arc";
8723
8784
  const encoding = spec.encoding;
@@ -8891,7 +8952,8 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
8891
8952
  width: Math.max(0, width - margins.left - margins.right),
8892
8953
  height: Math.max(0, height - margins.top - margins.bottom)
8893
8954
  };
8894
- if ((chartArea.width < MIN_CHART_WIDTH || chartArea.height < MIN_CHART_HEIGHT) && chromeMode !== "hidden") {
8955
+ const minDims = getMinChartDims(spec.display);
8956
+ if ((chartArea.width < minDims.width || chartArea.height < minDims.height) && chromeMode !== "hidden") {
8895
8957
  const fallbackMode = chromeMode === "full" ? "compact" : "hidden";
8896
8958
  const fallbackChrome = computeChrome2(
8897
8959
  chromeToInput(spec.chrome),
@@ -9438,7 +9500,8 @@ function truncateEntries(entries, maxCount) {
9438
9500
  return truncated;
9439
9501
  }
9440
9502
  function computeLegend(spec, strategy, theme, chartArea, watermark = true) {
9441
- if (spec.legend?.show === false || strategy.legendMaxHeight === 0) {
9503
+ const sparklineHidden = spec.display === "sparkline" && !spec.userExplicit.legend;
9504
+ if (sparklineHidden || spec.legend?.show === false || strategy.legendMaxHeight === 0) {
9442
9505
  return {
9443
9506
  position: "top",
9444
9507
  entries: [],
@@ -12176,7 +12239,7 @@ function compileChart(spec, options) {
12176
12239
  }
12177
12240
  let chartSpec = normalized;
12178
12241
  const rawWatermark = expandedSpec.watermark;
12179
- const watermark = rawWatermark !== void 0 ? chartSpec.watermark : options.watermark ?? true;
12242
+ let watermark = rawWatermark !== void 0 ? chartSpec.watermark : options.watermark ?? true;
12180
12243
  const rawTransforms = expandedSpec.transform;
12181
12244
  if (rawTransforms && rawTransforms.length > 0) {
12182
12245
  chartSpec = { ...chartSpec, data: runTransforms(chartSpec.data, rawTransforms) };
@@ -12186,6 +12249,21 @@ function compileChart(spec, options) {
12186
12249
  let strategy = getLayoutStrategy(breakpoint, heightClass);
12187
12250
  const rawSpec = expandedSpec;
12188
12251
  const overrides = rawSpec.overrides;
12252
+ const rawEncoding = rawSpec.encoding;
12253
+ const bpForExplicit = overrides?.[breakpoint];
12254
+ const bpEncoding = bpForExplicit?.encoding;
12255
+ const hasChromeKeys = (v) => !!v && typeof v === "object" && Object.keys(v).length > 0;
12256
+ const userExplicit = {
12257
+ chrome: hasChromeKeys(rawSpec.chrome) || hasChromeKeys(bpForExplicit?.chrome),
12258
+ legend: rawSpec.legend !== void 0 || bpForExplicit?.legend !== void 0,
12259
+ xAxis: rawEncoding?.x?.axis !== void 0 || bpEncoding?.x?.axis !== void 0,
12260
+ yAxis: rawEncoding?.y?.axis !== void 0 || bpEncoding?.y?.axis !== void 0,
12261
+ labels: rawSpec.labels !== void 0 || bpForExplicit?.labels !== void 0,
12262
+ animation: rawSpec.animation !== void 0 || bpForExplicit?.animation !== void 0,
12263
+ watermark: rawSpec.watermark !== void 0 || bpForExplicit?.watermark !== void 0,
12264
+ crosshair: rawSpec.crosshair !== void 0 || bpForExplicit?.crosshair !== void 0
12265
+ };
12266
+ chartSpec = { ...chartSpec, userExplicit };
12189
12267
  if (overrides?.[breakpoint]) {
12190
12268
  const bp = overrides[breakpoint];
12191
12269
  if (bp.chrome) {
@@ -12229,9 +12307,80 @@ function compileChart(spec, options) {
12229
12307
  };
12230
12308
  strategy = { ...strategy, annotationPosition: "inline" };
12231
12309
  }
12310
+ if (bp.display !== void 0) {
12311
+ chartSpec = {
12312
+ ...chartSpec,
12313
+ display: bp.display
12314
+ };
12315
+ }
12316
+ if (bp.encoding !== void 0) {
12317
+ const bpEnc = bp.encoding;
12318
+ const mergedEncoding = { ...chartSpec.encoding };
12319
+ const NESTED_CHANNEL_KEYS = ["axis", "scale"];
12320
+ for (const channel of Object.keys(bpEnc)) {
12321
+ const baseCh = mergedEncoding[channel];
12322
+ const bpCh = bpEnc[channel];
12323
+ if (bpCh && baseCh) {
12324
+ const merged = { ...baseCh, ...bpCh };
12325
+ for (const key of NESTED_CHANNEL_KEYS) {
12326
+ const baseNested = baseCh[key];
12327
+ const bpNested = bpCh[key];
12328
+ if (baseNested && bpNested && typeof baseNested === "object" && typeof bpNested === "object" && !Array.isArray(baseNested) && !Array.isArray(bpNested)) {
12329
+ merged[key] = { ...baseNested, ...bpNested };
12330
+ }
12331
+ }
12332
+ mergedEncoding[channel] = merged;
12333
+ } else if (bpCh) {
12334
+ mergedEncoding[channel] = bpCh;
12335
+ }
12336
+ }
12337
+ chartSpec = {
12338
+ ...chartSpec,
12339
+ encoding: mergedEncoding
12340
+ };
12341
+ }
12342
+ if (typeof bp.watermark === "boolean") {
12343
+ watermark = bp.watermark;
12344
+ chartSpec = { ...chartSpec, watermark };
12345
+ }
12346
+ }
12347
+ if (chartSpec.display === "sparkline" && !chartSpec.userExplicit.labels) {
12348
+ chartSpec = {
12349
+ ...chartSpec,
12350
+ labels: { ...chartSpec.labels, density: "none" }
12351
+ };
12352
+ }
12353
+ let rawAnimationSpec = overrides?.[breakpoint]?.animation ?? rawSpec.animation;
12354
+ if (rawAnimationSpec === void 0 && chartSpec.display === "sparkline") {
12355
+ rawAnimationSpec = false;
12356
+ }
12357
+ if (chartSpec.display === "sparkline" && rawAnimationSpec !== false && rawAnimationSpec !== void 0) {
12358
+ const SPARK_DURATION = 1100;
12359
+ if (rawAnimationSpec === true) {
12360
+ rawAnimationSpec = { enter: { duration: SPARK_DURATION } };
12361
+ } else if (typeof rawAnimationSpec === "object") {
12362
+ const cfg = rawAnimationSpec;
12363
+ const enter = cfg.enter;
12364
+ if (enter === void 0 || enter === true) {
12365
+ rawAnimationSpec = {
12366
+ ...cfg,
12367
+ enter: { duration: SPARK_DURATION }
12368
+ };
12369
+ } else if (typeof enter === "object" && enter !== null && enter.duration === void 0) {
12370
+ rawAnimationSpec = {
12371
+ ...cfg,
12372
+ enter: { ...enter, duration: SPARK_DURATION }
12373
+ };
12374
+ }
12375
+ }
12232
12376
  }
12233
- const rawAnimationSpec = overrides?.[breakpoint]?.animation ?? rawSpec.animation;
12234
12377
  const resolvedAnimation = resolveAnimation(rawAnimationSpec);
12378
+ const rawCrosshair = bpForExplicit?.crosshair ?? rawSpec.crosshair;
12379
+ const crosshair = chartSpec.display === "sparkline" && !chartSpec.userExplicit.crosshair ? false : rawCrosshair === true;
12380
+ if (chartSpec.display === "sparkline" && !chartSpec.userExplicit.watermark) {
12381
+ watermark = false;
12382
+ chartSpec = { ...chartSpec, watermark: false };
12383
+ }
12235
12384
  const mergedThemeConfig = options.theme ? { ...chartSpec.theme, ...options.theme } : chartSpec.theme;
12236
12385
  let theme = resolveTheme4(mergedThemeConfig);
12237
12386
  if (options.darkMode) {
@@ -12276,9 +12425,13 @@ function compileChart(spec, options) {
12276
12425
  applyColorScaleRange(scales, renderSpec.encoding, theme);
12277
12426
  scales.defaultColor = chartSpec.markDef.fill ?? chartSpec.markDef.stroke ?? theme.colors.categorical[0];
12278
12427
  const isRadial = chartSpec.markType === "arc";
12428
+ const skipX = chartSpec.display === "sparkline" && !chartSpec.userExplicit.xAxis;
12429
+ const skipY = chartSpec.display === "sparkline" && !chartSpec.userExplicit.yAxis;
12279
12430
  const axes = isRadial ? { x: void 0, y: void 0 } : computeAxes(scales, chartArea, strategy, theme, options.measureText, {
12280
12431
  data: renderSpec.data,
12281
- encoding: renderSpec.encoding
12432
+ encoding: renderSpec.encoding,
12433
+ skipX,
12434
+ skipY
12282
12435
  });
12283
12436
  if (!isRadial) {
12284
12437
  computeGridlines(axes, chartArea);
@@ -12354,6 +12507,8 @@ function compileChart(spec, options) {
12354
12507
  },
12355
12508
  animation: resolvedAnimation,
12356
12509
  watermark,
12510
+ display: chartSpec.display,
12511
+ crosshair,
12357
12512
  measureText: options.measureText
12358
12513
  };
12359
12514
  }