@opendata-ai/openchart-engine 2.1.0 → 2.2.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, CompileTableOptions, TableLayout, ChartType, DataRow, Encoding, ChromeText, Annotation, LabelConfig, LegendConfig, ThemeConfig, DarkMode, ColumnConfig, GraphSpec, GraphEncoding, GraphLayoutConfig, VizSpec, EncodingChannel, Rect, LayoutStrategy, Mark } from '@opendata-ai/openchart-core';
2
+ import { LegendLayout, ResolvedChrome, TooltipContent, A11yMetadata, ResolvedTheme, CompileOptions, ChartLayout, CompileTableOptions, TableLayout, ChartType, DataRow, Encoding, ChromeText, Annotation, LabelConfig, LegendConfig, ThemeConfig, DarkMode, ColumnConfig, GraphSpec, GraphEncoding, GraphLayoutConfig, NodeOverride, VizSpec, EncodingChannel, Rect, LayoutStrategy, Mark } from '@opendata-ai/openchart-core';
3
3
  export { ChartLayout, ChartSpec, CompileOptions, CompileTableOptions, GraphLayout, GraphSpec, TableLayout, TableSpec, VizSpec } from '@opendata-ai/openchart-core';
4
4
  import { ScaleLinear, ScaleTime, ScaleLogarithmic, ScaleBand, ScalePoint, ScaleOrdinal } from 'd3-scale';
5
5
 
@@ -65,6 +65,12 @@ interface SimulationConfig {
65
65
  velocityDecay: number;
66
66
  /** Collision radius: max node radius + padding. */
67
67
  collisionRadius: number;
68
+ /** Extra px added to node radius for collision (default 2). */
69
+ collisionPadding?: number;
70
+ /** Link force strength override. */
71
+ linkStrength?: number;
72
+ /** Whether to apply center force (default true). */
73
+ centerForce?: boolean;
68
74
  }
69
75
  /**
70
76
  * The complete engine output for graph specs.
@@ -172,6 +178,8 @@ interface NormalizedChartSpec {
172
178
  responsive: boolean;
173
179
  theme: ThemeConfig;
174
180
  darkMode: DarkMode;
181
+ /** Series names to hide from rendering. */
182
+ hiddenSeries: string[];
175
183
  }
176
184
  /** A TableSpec with all optional fields filled with sensible defaults. */
177
185
  interface NormalizedTableSpec {
@@ -197,6 +205,7 @@ interface NormalizedGraphSpec {
197
205
  edges: GraphSpec['edges'];
198
206
  encoding: GraphEncoding;
199
207
  layout: GraphLayoutConfig;
208
+ nodeOverrides?: Record<string, NodeOverride>;
200
209
  chrome: NormalizedChrome;
201
210
  annotations: Annotation[];
202
211
  theme: ThemeConfig;
package/dist/index.js CHANGED
@@ -1898,7 +1898,8 @@ function normalizeChartSpec(spec, warnings) {
1898
1898
  legend: spec.legend,
1899
1899
  responsive: spec.responsive ?? true,
1900
1900
  theme: spec.theme ?? {},
1901
- darkMode: spec.darkMode ?? "off"
1901
+ darkMode: spec.darkMode ?? "off",
1902
+ hiddenSeries: spec.hiddenSeries ?? []
1902
1903
  };
1903
1904
  }
1904
1905
  function normalizeTableSpec(spec, _warnings) {
@@ -1933,6 +1934,7 @@ function normalizeGraphSpec(spec, _warnings) {
1933
1934
  edges: spec.edges,
1934
1935
  encoding: spec.encoding ?? {},
1935
1936
  layout,
1937
+ nodeOverrides: spec.nodeOverrides,
1936
1938
  chrome: normalizeChrome(spec.chrome),
1937
1939
  annotations: normalizeAnnotations(spec.annotations),
1938
1940
  theme: spec.theme ?? {},
@@ -2463,7 +2465,7 @@ function computeDegrees(nodes, edges) {
2463
2465
  }
2464
2466
  return degrees;
2465
2467
  }
2466
- function resolveNodeVisuals(nodes, encoding, edges, theme) {
2468
+ function resolveNodeVisuals(nodes, encoding, edges, theme, nodeOverrides) {
2467
2469
  const degrees = computeDegrees(nodes, edges);
2468
2470
  const maxDegree = Math.max(1, ...degrees.values());
2469
2471
  let sizeScale;
@@ -2517,14 +2519,20 @@ function resolveNodeVisuals(nodes, encoding, edges, theme) {
2517
2519
  const labelPriority = maxDegree > 0 ? degree / maxDegree : 0;
2518
2520
  const { id: _id, ...rest } = node;
2519
2521
  const data = { id: node.id, ...rest };
2522
+ const override = nodeOverrides?.[node.id];
2523
+ const finalFill = override?.fill ?? fill;
2524
+ const finalRadius = override?.radius ?? radius;
2525
+ const finalStrokeWidth = override?.strokeWidth ?? DEFAULT_STROKE_WIDTH2;
2526
+ const finalStroke = override?.stroke ?? stroke;
2527
+ const finalLabelPriority = override?.alwaysShowLabel ? Infinity : labelPriority;
2520
2528
  return {
2521
2529
  id: node.id,
2522
- radius,
2523
- fill,
2524
- stroke,
2525
- strokeWidth: DEFAULT_STROKE_WIDTH2,
2530
+ radius: finalRadius,
2531
+ fill: finalFill,
2532
+ stroke: finalStroke,
2533
+ strokeWidth: finalStrokeWidth,
2526
2534
  label,
2527
- labelPriority,
2535
+ labelPriority: finalLabelPriority,
2528
2536
  community: void 0,
2529
2537
  data
2530
2538
  };
@@ -2561,6 +2569,17 @@ function resolveEdgeVisuals(edges, encoding, theme) {
2561
2569
  }
2562
2570
  }
2563
2571
  const defaultEdgeColor = hexWithOpacity(theme.colors.axis, 0.4);
2572
+ const EDGE_STYLES = ["solid", "dashed", "dotted"];
2573
+ let styleFn;
2574
+ if (encoding.edgeStyle?.field) {
2575
+ const field = encoding.edgeStyle.field;
2576
+ const uniqueValues = [...new Set(edges.map((e) => String(e[field] ?? "")))];
2577
+ const styleMap = /* @__PURE__ */ new Map();
2578
+ for (let i = 0; i < uniqueValues.length; i++) {
2579
+ styleMap.set(uniqueValues[i], EDGE_STYLES[i % EDGE_STYLES.length]);
2580
+ }
2581
+ styleFn = (edge) => styleMap.get(String(edge[field] ?? "")) ?? "solid";
2582
+ }
2564
2583
  return edges.map((edge) => {
2565
2584
  const { source, target, ...rest } = edge;
2566
2585
  let strokeWidth = DEFAULT_EDGE_WIDTH;
@@ -2571,12 +2590,13 @@ function resolveEdgeVisuals(edges, encoding, theme) {
2571
2590
  }
2572
2591
  }
2573
2592
  const stroke = edgeColorFn ? edgeColorFn(edge) : defaultEdgeColor;
2593
+ const style = styleFn ? styleFn(edge) : "solid";
2574
2594
  return {
2575
2595
  source,
2576
2596
  target,
2577
2597
  stroke,
2578
2598
  strokeWidth,
2579
- style: "solid",
2599
+ style,
2580
2600
  data: { source, target, ...rest }
2581
2601
  };
2582
2602
  });
@@ -2705,7 +2725,8 @@ function compileGraph(spec, options) {
2705
2725
  graphSpec.nodes,
2706
2726
  graphSpec.encoding,
2707
2727
  graphSpec.edges,
2708
- theme
2728
+ theme,
2729
+ graphSpec.nodeOverrides
2709
2730
  );
2710
2731
  const clusteringField = graphSpec.layout.clustering?.field;
2711
2732
  const hasCommunities = !!clusteringField;
@@ -2731,6 +2752,7 @@ function compileGraph(spec, options) {
2731
2752
  role: "img",
2732
2753
  keyboardNavigable: compiledNodes.length > 0
2733
2754
  };
2755
+ const collisionPadding = graphSpec.layout.collisionPadding ?? 2;
2734
2756
  const maxRadius = compiledNodes.length > 0 ? Math.max(...compiledNodes.map((n) => n.radius)) : DEFAULT_COLLISION_PADDING;
2735
2757
  const simulationConfig = {
2736
2758
  chargeStrength: graphSpec.layout.chargeStrength ?? -300,
@@ -2738,7 +2760,10 @@ function compileGraph(spec, options) {
2738
2760
  clustering: clusteringField ? { field: clusteringField, strength: 0.5 } : null,
2739
2761
  alphaDecay: 0.0228,
2740
2762
  velocityDecay: 0.4,
2741
- collisionRadius: maxRadius + 2
2763
+ collisionRadius: maxRadius + collisionPadding,
2764
+ collisionPadding,
2765
+ linkStrength: graphSpec.layout.linkStrength,
2766
+ centerForce: graphSpec.layout.centerForce
2742
2767
  };
2743
2768
  const chrome = computeChrome(
2744
2769
  {
@@ -4182,7 +4207,27 @@ function compileChart(spec, options) {
4182
4207
  }
4183
4208
  }
4184
4209
  const finalLegend = computeLegend(chartSpec, strategy, theme, legendArea);
4185
- const scales = computeScales(chartSpec, chartArea, chartSpec.data);
4210
+ let renderData = chartSpec.data;
4211
+ if (chartSpec.hiddenSeries.length > 0 && chartSpec.encoding.color) {
4212
+ const colorField = chartSpec.encoding.color.field;
4213
+ const hiddenSet = new Set(chartSpec.hiddenSeries);
4214
+ renderData = renderData.filter((row) => !hiddenSet.has(String(row[colorField])));
4215
+ }
4216
+ for (const channel of ["x", "y"]) {
4217
+ const enc = chartSpec.encoding[channel];
4218
+ if (!enc?.scale?.clip || !enc.scale.domain) continue;
4219
+ const domain = enc.scale.domain;
4220
+ const field = enc.field;
4221
+ if (Array.isArray(domain) && domain.length === 2 && typeof domain[0] === "number") {
4222
+ const [lo, hi] = domain;
4223
+ renderData = renderData.filter((row) => {
4224
+ const v = Number(row[field]);
4225
+ return Number.isFinite(v) && v >= lo && v <= hi;
4226
+ });
4227
+ }
4228
+ }
4229
+ const renderSpec = renderData !== chartSpec.data ? { ...chartSpec, data: renderData } : chartSpec;
4230
+ const scales = computeScales(renderSpec, chartArea, renderSpec.data);
4186
4231
  if (scales.color) {
4187
4232
  if (scales.color.type === "sequential") {
4188
4233
  const seqStops = Object.values(theme.colors.sequential)[0] ?? theme.colors.categorical;
@@ -4202,8 +4247,8 @@ function compileChart(spec, options) {
4202
4247
  if (!isRadial) {
4203
4248
  computeGridlines(axes, chartArea);
4204
4249
  }
4205
- const renderer = getChartRenderer(chartSpec.type);
4206
- const marks = renderer ? renderer(chartSpec, scales, chartArea, strategy, theme) : [];
4250
+ const renderer = getChartRenderer(renderSpec.type);
4251
+ const marks = renderer ? renderer(renderSpec, scales, chartArea, strategy, theme) : [];
4207
4252
  const obstacles = [];
4208
4253
  if (finalLegend.bounds.width > 0) {
4209
4254
  obstacles.push(finalLegend.bounds);