@opendata-ai/openchart-engine 8.0.0 → 8.1.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
@@ -200,6 +200,12 @@ interface GraphCompilation {
200
200
  field: string;
201
201
  values: string[];
202
202
  };
203
+ /**
204
+ * Ids exempt from highlight/filter dimming (currently just `spec.seedNode`).
205
+ * Empty when no seed is set. Array-shaped so a future `pinnedNodes` needs no
206
+ * type change.
207
+ */
208
+ seedNodeIds: string[];
203
209
  /** Edge legend entries (nominal edgeColor with >1 category), or undefined. */
204
210
  edgeLegend?: LegendEntry[];
205
211
  }
@@ -580,6 +586,11 @@ interface NormalizedGraphSpec {
580
586
  encoding: GraphEncoding;
581
587
  layout: GraphLayoutConfig;
582
588
  nodeOverrides?: Record<string, NodeOverride>;
589
+ /** Seed node, always in object form (the string shorthand is expanded). */
590
+ seedNode?: {
591
+ id: string;
592
+ style?: NodeOverride;
593
+ };
583
594
  chrome: NormalizedChrome;
584
595
  annotations: Annotation[];
585
596
  theme: ThemeConfig;
@@ -861,6 +872,21 @@ declare function validateSpec(spec: unknown): ValidationResult;
861
872
  */
862
873
  declare function compile(spec: unknown): CompileResult;
863
874
 
875
+ /**
876
+ * Resolve which backend paints a chart's point marks: SVG (one `<circle>` per
877
+ * datum) or a canvas mark layer (batched paint calls).
878
+ *
879
+ * High-cardinality scatter plots are the only case where the trade is worth
880
+ * it, so canvas is refused for every other mark type and for the layout shapes
881
+ * the canvas layer cannot express (facets, layers, sparklines). Refusals are
882
+ * advisory: an explicit `'canvas'` request always warns, and an `'auto'`
883
+ * refusal warns only once the chart is dense enough for the SVG fallback to
884
+ * actually hurt (AUTO_CANVAS_REFUSAL_WARN_THRESHOLD).
885
+ */
886
+
887
+ /** Point count above which `'auto'` prefers canvas over SVG. */
888
+ declare const AUTO_CANVAS_THRESHOLD = 1000;
889
+
864
890
  /**
865
891
  * Chart renderer registry.
866
892
  *
@@ -1058,4 +1084,4 @@ declare function runTimeUnit(data: DataRow[], transform: TimeUnitTransform): Dat
1058
1084
  */
1059
1085
  declare function runTransforms(data: DataRow[], transforms: Transform[]): DataRow[];
1060
1086
 
1061
- export { ARROWHEAD_LENGTH, type AnnotationContext, type AnnotationMeasureTextFn, type ArrowheadPoints, BOLD_SPAN_FONT_WEIGHT, type ChartRenderer, type CompileResult, type CompiledGraphEdge, type CompiledGraphNode, DEFAULT_ANNOTATION_FONT_SIZE, DEFAULT_LINE_HEIGHT, ENTER_DEFAULTS, EXIT_DEFAULTS, type GraphCompilation, type NormalizedBarListSpec, type NormalizedChartSpec, type NormalizedChrome, type NormalizedGeoMapSpec, type NormalizedGraphSpec, type NormalizedSankeySpec, type NormalizedSpec, type NormalizedTableSpec, type NormalizedTileMapSpec, type ObstacleKind, type PlacementObstacle, type RichSpan, type SimulationConfig, UPDATE_DEFAULTS, type ValidationError, type ValidationErrorCode, type ValidationResult, buildAreaPath, buildEdgeTooltip, buildLinePath, clampStaggerDelay, clearRenderers, compile, compileBarList, compileChart, compileGeoMap, compileGraph, compileLayer, compileSankey, compileTable, compileTileMap, computeArrowheadPoints, dedupeKeys, evaluatePredicate, facetMinHeight, getChartRenderer, heuristicMeasure, isConditionalValueDef, legendGap, normalizeSpec, parseAnnotationSpans, registerChartRenderer, resolveAnimation, resolveConditionalValue, runBin, runCalculate, runFilter, runTimeUnit, runTransforms, serializeKeyValue, validateSpec };
1087
+ export { ARROWHEAD_LENGTH, AUTO_CANVAS_THRESHOLD, type AnnotationContext, type AnnotationMeasureTextFn, type ArrowheadPoints, BOLD_SPAN_FONT_WEIGHT, type ChartRenderer, type CompileResult, type CompiledGraphEdge, type CompiledGraphNode, DEFAULT_ANNOTATION_FONT_SIZE, DEFAULT_LINE_HEIGHT, ENTER_DEFAULTS, EXIT_DEFAULTS, type GraphCompilation, type NormalizedBarListSpec, type NormalizedChartSpec, type NormalizedChrome, type NormalizedGeoMapSpec, type NormalizedGraphSpec, type NormalizedSankeySpec, type NormalizedSpec, type NormalizedTableSpec, type NormalizedTileMapSpec, type ObstacleKind, type PlacementObstacle, type RichSpan, type SimulationConfig, UPDATE_DEFAULTS, type ValidationError, type ValidationErrorCode, type ValidationResult, buildAreaPath, buildEdgeTooltip, buildLinePath, clampStaggerDelay, clearRenderers, compile, compileBarList, compileChart, compileGeoMap, compileGraph, compileLayer, compileSankey, compileTable, compileTileMap, computeArrowheadPoints, dedupeKeys, evaluatePredicate, facetMinHeight, getChartRenderer, heuristicMeasure, isConditionalValueDef, legendGap, normalizeSpec, parseAnnotationSpans, registerChartRenderer, resolveAnimation, resolveConditionalValue, runBin, runCalculate, runFilter, runTimeUnit, runTransforms, serializeKeyValue, validateSpec };
package/dist/index.js CHANGED
@@ -10595,11 +10595,16 @@ function normalizeSankeySpec(spec, _warnings) {
10595
10595
  watermark: spec.watermark ?? true
10596
10596
  };
10597
10597
  }
10598
- function normalizeGraphSpec(spec) {
10598
+ function normalizeGraphSpec(spec, warnings) {
10599
10599
  const layout = {
10600
10600
  type: "force",
10601
10601
  ...spec.layout
10602
10602
  };
10603
+ let seedNode = typeof spec.seedNode === "string" ? { id: spec.seedNode } : spec.seedNode;
10604
+ if (seedNode && !spec.nodes.some((n) => n.id === seedNode?.id)) {
10605
+ warnings.push(`seedNode "${seedNode.id}" does not match any node id; it will be ignored.`);
10606
+ seedNode = void 0;
10607
+ }
10603
10608
  return {
10604
10609
  type: "graph",
10605
10610
  nodes: spec.nodes,
@@ -10607,6 +10612,7 @@ function normalizeGraphSpec(spec) {
10607
10612
  encoding: spec.encoding ?? {},
10608
10613
  layout,
10609
10614
  nodeOverrides: spec.nodeOverrides,
10615
+ seedNode,
10610
10616
  chrome: normalizeChrome(spec.chrome),
10611
10617
  annotations: normalizeAnnotations(spec.annotations),
10612
10618
  theme: spec.theme ?? {},
@@ -10722,7 +10728,7 @@ function normalizeSpec(spec, warnings = []) {
10722
10728
  return normalizeTableSpec(spec, warnings);
10723
10729
  }
10724
10730
  if (isGraphSpec(spec)) {
10725
- return normalizeGraphSpec(spec);
10731
+ return normalizeGraphSpec(spec, warnings);
10726
10732
  }
10727
10733
  if (isSankeySpec(spec)) {
10728
10734
  return normalizeSankeySpec(spec, warnings);
@@ -11624,6 +11630,35 @@ function validateGraphSpec(spec, errors) {
11624
11630
  nodeIds.add(node.id);
11625
11631
  }
11626
11632
  }
11633
+ if (spec.seedNode !== void 0) {
11634
+ if (typeof spec.seedNode === "string") {
11635
+ if (spec.seedNode === "") {
11636
+ errors.push({
11637
+ message: 'Spec error: "seedNode" must be a non-empty node id',
11638
+ path: "seedNode",
11639
+ code: "MISSING_FIELD",
11640
+ suggestion: `Use one of the existing node ids, e.g. seedNode: "${[...nodeIds][0] ?? "a"}"`
11641
+ });
11642
+ }
11643
+ } else if (typeof spec.seedNode !== "object" || spec.seedNode === null || Array.isArray(spec.seedNode)) {
11644
+ errors.push({
11645
+ message: `Spec error: "seedNode" must be a node id string or an object like { id, style }, got ${Array.isArray(spec.seedNode) ? "array" : typeof spec.seedNode}`,
11646
+ path: "seedNode",
11647
+ code: "INVALID_TYPE",
11648
+ suggestion: `Use the id shorthand (seedNode: "${[...nodeIds][0] ?? "a"}") or the object form (seedNode: { id: "${[...nodeIds][0] ?? "a"}", style: { radius: 14 } })`
11649
+ });
11650
+ } else {
11651
+ const seedId = spec.seedNode.id;
11652
+ if (typeof seedId !== "string" || seedId === "") {
11653
+ errors.push({
11654
+ message: 'Spec error: "seedNode" must have a non-empty string "id" field',
11655
+ path: "seedNode.id",
11656
+ code: "MISSING_FIELD",
11657
+ suggestion: `Add the seed node id, e.g. seedNode: { id: "${[...nodeIds][0] ?? "a"}" }`
11658
+ });
11659
+ }
11660
+ }
11661
+ }
11627
11662
  if (!Array.isArray(spec.edges)) {
11628
11663
  errors.push({
11629
11664
  message: 'Spec error: graph spec requires an "edges" array',
@@ -13513,9 +13548,9 @@ function buildBandScale(channel, data, rangeStart, rangeEnd, defaultPadding = DE
13513
13548
  }
13514
13549
  return { scale, type: "band", channel };
13515
13550
  }
13516
- function buildPointScale(channel, data, rangeStart, rangeEnd) {
13551
+ function buildPointScale(channel, data, rangeStart, rangeEnd, defaultPadding = 0.5) {
13517
13552
  const values = channel.scale?.domain ? channel.scale.domain : applyCategoricalSort(uniqueStrings(fieldValues(data, channel.field)), channel.sort);
13518
- const padding = channel.scale?.padding ?? channel.scale?.paddingOuter ?? 0.5;
13553
+ const padding = channel.scale?.padding ?? channel.scale?.paddingOuter ?? defaultPadding;
13519
13554
  const scale = point().domain(values).range([rangeStart, rangeEnd]).padding(padding);
13520
13555
  if (channel.scale?.reverse) {
13521
13556
  const [r0, r1] = scale.range();
@@ -13592,7 +13627,7 @@ function buildPositionalScale(channel, data, rangeStart, rangeEnd, chartType, ax
13592
13627
  case "point":
13593
13628
  return buildPointScale(channel, data, rangeStart, rangeEnd);
13594
13629
  case "ordinal":
13595
- return buildBandScale(channel, data, rangeStart, rangeEnd);
13630
+ return buildCategoricalScale(channel, data, rangeStart, rangeEnd, chartType, axis);
13596
13631
  }
13597
13632
  }
13598
13633
  switch (channel.type) {
@@ -13602,17 +13637,21 @@ function buildPositionalScale(channel, data, rangeStart, rangeEnd, chartType, ax
13602
13637
  return buildLinearScale(channel, data, rangeStart, rangeEnd);
13603
13638
  case "nominal":
13604
13639
  case "ordinal":
13605
- if (chartType === "rect") {
13606
- return buildBandScale(channel, data, rangeStart, rangeEnd, HEATMAP_BAND_PADDING);
13607
- }
13608
- if (chartType === "bar" || chartType === "beeswarm" || chartType === "range" || (chartType === "circle" || chartType === "lollipop") && axis === "y") {
13609
- return buildBandScale(channel, data, rangeStart, rangeEnd);
13610
- }
13611
- return buildPointScale(channel, data, rangeStart, rangeEnd);
13640
+ return buildCategoricalScale(channel, data, rangeStart, rangeEnd, chartType, axis);
13612
13641
  default:
13613
13642
  return buildLinearScale(channel, data, rangeStart, rangeEnd);
13614
13643
  }
13615
13644
  }
13645
+ function buildCategoricalScale(channel, data, rangeStart, rangeEnd, chartType, axis) {
13646
+ if (chartType === "rect") {
13647
+ return buildBandScale(channel, data, rangeStart, rangeEnd, HEATMAP_BAND_PADDING);
13648
+ }
13649
+ if (chartType === "bar" || chartType === "beeswarm" || chartType === "range" || (chartType === "circle" || chartType === "lollipop") && axis === "y") {
13650
+ return buildBandScale(channel, data, rangeStart, rangeEnd);
13651
+ }
13652
+ const isTrendMark = chartType === "line" || chartType === "area";
13653
+ return buildPointScale(channel, data, rangeStart, rangeEnd, isTrendMark ? 0 : 0.5);
13654
+ }
13616
13655
  function computeScales(spec, chartArea, data) {
13617
13656
  const result = {};
13618
13657
  const encoding = spec.encoding;
@@ -14764,6 +14803,32 @@ function applyBandLabelStride(ticks2, stride) {
14764
14803
  const lastIdx = ticks2.length - 1;
14765
14804
  return ticks2.filter((_, i) => (lastIdx - i) % stride === 0);
14766
14805
  }
14806
+ var LABEL_EDGE_GUARD = 2;
14807
+ function clampXTickLabels(ticks2, totalWidth, tickAngle, fontSize, fontWeight, measureText) {
14808
+ const rotated = tickAngle !== void 0 && Math.abs(tickAngle) > 10;
14809
+ const rad = rotated ? Math.abs(tickAngle) * Math.PI / 180 : 0;
14810
+ const across = rotated ? fontSize * 1.2 * Math.sin(rad) / 2 : 0;
14811
+ return ticks2.map((tick) => {
14812
+ const width = measureLabel(tick.label, fontSize, fontWeight, measureText);
14813
+ let left2;
14814
+ let right2;
14815
+ if (rotated) {
14816
+ const along = width * Math.cos(rad);
14817
+ const trailsLeft = tickAngle < 0;
14818
+ left2 = trailsLeft ? -(along + across) : -across;
14819
+ right2 = trailsLeft ? across : along + across;
14820
+ } else {
14821
+ left2 = -width / 2;
14822
+ right2 = width / 2;
14823
+ }
14824
+ let x2 = tick.position;
14825
+ const overRight = x2 + right2 - (totalWidth - LABEL_EDGE_GUARD);
14826
+ if (overRight > 0) x2 -= overRight;
14827
+ const overLeft = LABEL_EDGE_GUARD - (x2 + left2);
14828
+ if (overLeft > 0) x2 += overLeft;
14829
+ return x2 === tick.position ? tick : { ...tick, labelPosition: x2 };
14830
+ });
14831
+ }
14767
14832
  function computeAxes(scales, chartArea, strategy, theme, measureText, dataContext) {
14768
14833
  const result = {};
14769
14834
  const baseDensity = strategy.axisLabelDensity;
@@ -14976,6 +15041,14 @@ function computeAxes(scales, chartArea, strategy, theme, measureText, dataContex
14976
15041
  const totalWidth = dataContext?.totalWidth ?? chartArea.x + chartArea.width;
14977
15042
  if (result.x) {
14978
15043
  const isRotated = !!result.x.tickAngle && Math.abs(result.x.tickAngle) > 10;
15044
+ result.x.ticks = clampXTickLabels(
15045
+ result.x.ticks,
15046
+ totalWidth,
15047
+ result.x.tickAngle,
15048
+ result.x.tickLabelStyle.fontSize,
15049
+ result.x.tickLabelStyle.fontWeight,
15050
+ measureText
15051
+ );
14979
15052
  result.x.extent = computeXAxisExtentFromLabels({
14980
15053
  labels: result.x.ticks.map((t) => t.label),
14981
15054
  tickAngle: result.x.tickAngle,
@@ -19498,6 +19571,16 @@ function resolveGraphInteraction(cfg) {
19498
19571
 
19499
19572
  // src/graphs/compile-graph.ts
19500
19573
  var graphNumberFormatter = defaultNumberFormatter8({ allIntegers: false, surface: "chart" });
19574
+ function mergeOverrides(...layers) {
19575
+ const merged = {};
19576
+ for (const layer of layers) {
19577
+ if (!layer) continue;
19578
+ for (const [key, value2] of Object.entries(layer)) {
19579
+ if (value2 !== void 0) merged[key] = value2;
19580
+ }
19581
+ }
19582
+ return merged;
19583
+ }
19501
19584
  var SWATCH_SIZE2 = 12;
19502
19585
  var SWATCH_GAP2 = 6;
19503
19586
  var ENTRY_GAP2 = 16;
@@ -19658,12 +19741,23 @@ function compileGraph(spec, options) {
19658
19741
  if (options.darkMode) {
19659
19742
  theme = adaptTheme3(theme);
19660
19743
  }
19744
+ const seedNodeIds = graphSpec.seedNode ? [graphSpec.seedNode.id] : [];
19745
+ const nodeOverrides = graphSpec.seedNode ? {
19746
+ ...graphSpec.nodeOverrides,
19747
+ [graphSpec.seedNode.id]: mergeOverrides(
19748
+ // Radius is deliberately absent so the seed doesn't fight a nodeSize
19749
+ // encoding; a host wanting a bigger seed sets seedNode.style.radius.
19750
+ { stroke: theme.colors.text, strokeWidth: 2, alwaysShowLabel: true },
19751
+ graphSpec.seedNode.style,
19752
+ graphSpec.nodeOverrides?.[graphSpec.seedNode.id]
19753
+ )
19754
+ } : graphSpec.nodeOverrides;
19661
19755
  const compiledNodes = resolveNodeVisuals(
19662
19756
  graphSpec.nodes,
19663
19757
  graphSpec.encoding,
19664
19758
  graphSpec.edges,
19665
19759
  theme,
19666
- graphSpec.nodeOverrides
19760
+ nodeOverrides
19667
19761
  );
19668
19762
  const clusteringField = graphSpec.layout.clustering?.field;
19669
19763
  const hasCommunities = !!clusteringField;
@@ -19769,6 +19863,7 @@ function compileGraph(spec, options) {
19769
19863
  interaction,
19770
19864
  legendField,
19771
19865
  initialHighlight,
19866
+ seedNodeIds,
19772
19867
  edgeLegend
19773
19868
  };
19774
19869
  }
@@ -23610,6 +23705,10 @@ function compileChart(spec, optionsInput) {
23610
23705
  watermark = false;
23611
23706
  chartSpec = { ...chartSpec, watermark: false };
23612
23707
  }
23708
+ if (heightClass === "cramped" && !chartSpec.userExplicit.watermark) {
23709
+ watermark = false;
23710
+ chartSpec = { ...chartSpec, watermark: false };
23711
+ }
23613
23712
  const mergedThemeConfig = options.theme ? { ...chartSpec.theme, ...options.theme } : chartSpec.theme;
23614
23713
  let theme = resolveTheme7(mergedThemeConfig);
23615
23714
  if (options.darkMode) {
@@ -24218,6 +24317,7 @@ function compileBarList2(spec, options) {
24218
24317
  }
24219
24318
  export {
24220
24319
  ARROWHEAD_LENGTH,
24320
+ AUTO_CANVAS_THRESHOLD,
24221
24321
  BOLD_SPAN_FONT_WEIGHT,
24222
24322
  DEFAULT_ANNOTATION_FONT_SIZE,
24223
24323
  DEFAULT_LINE_HEIGHT,