@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 +10 -1
- package/dist/index.js +58 -13
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/__test-fixtures__/specs.ts +3 -0
- package/src/__tests__/compile-chart.test.ts +123 -0
- package/src/compile.ts +33 -4
- package/src/compiler/normalize.ts +2 -0
- package/src/compiler/types.ts +4 -0
- package/src/graphs/__tests__/encoding.test.ts +101 -0
- package/src/graphs/compile-graph.ts +6 -1
- package/src/graphs/encoding.ts +30 -6
- package/src/graphs/types.ts +6 -0
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:
|
|
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
|
|
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 +
|
|
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
|
-
|
|
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(
|
|
4206
|
-
const marks = renderer ? renderer(
|
|
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);
|