@opendata-ai/openchart-engine 6.7.1 → 6.8.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 +2 -0
- package/dist/index.js +170 -31
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/legend.test.ts +30 -0
- package/src/annotations/__tests__/compute.test.ts +93 -0
- package/src/annotations/compute.ts +66 -13
- package/src/charts/bar/__tests__/compute.test.ts +67 -0
- package/src/charts/bar/compute.ts +69 -2
- package/src/compiler/normalize.ts +2 -0
- package/src/legend/compute.ts +6 -4
- package/src/sankey/__tests__/compile-sankey.test.ts +113 -8
- package/src/sankey/compile-sankey.ts +78 -13
- package/src/sankey/types.ts +2 -0
- package/src/tables/__tests__/format-cells.test.ts +17 -0
- package/src/tables/format-cells.ts +6 -10
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -296,19 +296,61 @@ function resolveRefLineAnnotation(annotation, scales, chartArea, isDark) {
|
|
|
296
296
|
let label;
|
|
297
297
|
if (annotation.label) {
|
|
298
298
|
const isHorizontal = annotation.y !== void 0;
|
|
299
|
-
const anchor = annotation.labelAnchor ?? "top";
|
|
300
|
-
|
|
301
|
-
|
|
299
|
+
const anchor = annotation.labelAnchor ?? (isHorizontal ? "top" : "left");
|
|
300
|
+
let baseDx;
|
|
301
|
+
let baseDy;
|
|
302
|
+
let labelX;
|
|
303
|
+
let labelY;
|
|
304
|
+
let textAnchor;
|
|
305
|
+
if (isHorizontal) {
|
|
306
|
+
if (anchor === "left") {
|
|
307
|
+
baseDx = 4;
|
|
308
|
+
baseDy = -4;
|
|
309
|
+
labelX = start.x;
|
|
310
|
+
labelY = start.y;
|
|
311
|
+
textAnchor = "start";
|
|
312
|
+
} else if (anchor === "bottom") {
|
|
313
|
+
baseDx = -4;
|
|
314
|
+
baseDy = 14;
|
|
315
|
+
labelX = end.x;
|
|
316
|
+
labelY = end.y;
|
|
317
|
+
textAnchor = "end";
|
|
318
|
+
} else {
|
|
319
|
+
baseDx = -4;
|
|
320
|
+
baseDy = -4;
|
|
321
|
+
labelX = end.x;
|
|
322
|
+
labelY = end.y;
|
|
323
|
+
textAnchor = "end";
|
|
324
|
+
}
|
|
325
|
+
} else {
|
|
326
|
+
if (anchor === "right") {
|
|
327
|
+
baseDx = -4;
|
|
328
|
+
baseDy = 14;
|
|
329
|
+
labelX = start.x;
|
|
330
|
+
labelY = start.y;
|
|
331
|
+
textAnchor = "end";
|
|
332
|
+
} else if (anchor === "bottom") {
|
|
333
|
+
baseDx = 4;
|
|
334
|
+
baseDy = -4;
|
|
335
|
+
labelX = start.x;
|
|
336
|
+
labelY = end.y;
|
|
337
|
+
textAnchor = "start";
|
|
338
|
+
} else {
|
|
339
|
+
baseDx = 4;
|
|
340
|
+
baseDy = 14;
|
|
341
|
+
labelX = start.x;
|
|
342
|
+
labelY = start.y;
|
|
343
|
+
textAnchor = "start";
|
|
344
|
+
}
|
|
345
|
+
}
|
|
302
346
|
const labelDelta = applyOffset({ dx: baseDx, dy: baseDy }, annotation.labelOffset);
|
|
303
347
|
const defaultStroke2 = isDark ? DARK_REFLINE_STROKE : LIGHT_REFLINE_STROKE;
|
|
304
348
|
const style = makeAnnotationLabelStyle(11, 400, annotation.stroke ?? defaultStroke2, isDark);
|
|
305
|
-
|
|
306
|
-
style.textAnchor = "end";
|
|
307
|
-
}
|
|
349
|
+
style.textAnchor = textAnchor;
|
|
308
350
|
label = {
|
|
309
351
|
text: annotation.label,
|
|
310
|
-
x:
|
|
311
|
-
y:
|
|
352
|
+
x: labelX + labelDelta.dx,
|
|
353
|
+
y: labelY + labelDelta.dy,
|
|
312
354
|
style,
|
|
313
355
|
visible: true
|
|
314
356
|
};
|
|
@@ -729,7 +771,22 @@ function computeBarMarks(spec, scales, _chartArea, _strategy) {
|
|
|
729
771
|
conditionalColor
|
|
730
772
|
);
|
|
731
773
|
}
|
|
732
|
-
|
|
774
|
+
const categoryGroups = groupByField(spec.data, yChannel.field);
|
|
775
|
+
const needsStacking = Array.from(categoryGroups.values()).some((rows) => rows.length > 1);
|
|
776
|
+
if (needsStacking) {
|
|
777
|
+
return computeStackedBars(
|
|
778
|
+
spec.data,
|
|
779
|
+
xChannel.field,
|
|
780
|
+
yChannel.field,
|
|
781
|
+
colorField,
|
|
782
|
+
xScale,
|
|
783
|
+
yScale,
|
|
784
|
+
bandwidth,
|
|
785
|
+
baseline,
|
|
786
|
+
scales
|
|
787
|
+
);
|
|
788
|
+
}
|
|
789
|
+
return computeColoredBars(
|
|
733
790
|
spec.data,
|
|
734
791
|
xChannel.field,
|
|
735
792
|
yChannel.field,
|
|
@@ -777,6 +834,36 @@ function computeStackedBars(data, valueField, categoryField, colorField, xScale,
|
|
|
777
834
|
}
|
|
778
835
|
return marks;
|
|
779
836
|
}
|
|
837
|
+
function computeColoredBars(data, valueField, categoryField, colorField, xScale, yScale, bandwidth, baseline, scales) {
|
|
838
|
+
const marks = [];
|
|
839
|
+
for (const row of data) {
|
|
840
|
+
const category = String(row[categoryField] ?? "");
|
|
841
|
+
const value2 = Number(row[valueField] ?? 0);
|
|
842
|
+
if (!Number.isFinite(value2)) continue;
|
|
843
|
+
const bandY = yScale(category);
|
|
844
|
+
if (bandY === void 0) continue;
|
|
845
|
+
const groupKey = String(row[colorField] ?? "");
|
|
846
|
+
const color2 = getColor(scales, groupKey);
|
|
847
|
+
const xPos = value2 >= 0 ? baseline : xScale(value2);
|
|
848
|
+
const barWidth = Math.max(Math.abs(xScale(value2) - baseline), MIN_BAR_WIDTH);
|
|
849
|
+
const aria = {
|
|
850
|
+
label: `${category}, ${groupKey}: ${formatBarValue(value2)}`
|
|
851
|
+
};
|
|
852
|
+
marks.push({
|
|
853
|
+
type: "rect",
|
|
854
|
+
x: xPos,
|
|
855
|
+
y: bandY,
|
|
856
|
+
width: barWidth,
|
|
857
|
+
height: bandwidth,
|
|
858
|
+
fill: color2,
|
|
859
|
+
cornerRadius: 2,
|
|
860
|
+
data: row,
|
|
861
|
+
aria,
|
|
862
|
+
orient: "horizontal"
|
|
863
|
+
});
|
|
864
|
+
}
|
|
865
|
+
return marks;
|
|
866
|
+
}
|
|
780
867
|
function computeSimpleBars(data, valueField, categoryField, xScale, yScale, bandwidth, baseline, scales, sequentialColor = false, conditionalColor) {
|
|
781
868
|
const marks = [];
|
|
782
869
|
for (const row of data) {
|
|
@@ -6028,7 +6115,9 @@ function normalizeSankeySpec(spec, _warnings) {
|
|
|
6028
6115
|
legend: spec.legend,
|
|
6029
6116
|
theme: spec.theme ?? {},
|
|
6030
6117
|
darkMode: spec.darkMode ?? "off",
|
|
6031
|
-
animation: spec.animation
|
|
6118
|
+
animation: spec.animation,
|
|
6119
|
+
valueFormat: spec.valueFormat,
|
|
6120
|
+
linkOpacity: spec.linkOpacity
|
|
6032
6121
|
};
|
|
6033
6122
|
}
|
|
6034
6123
|
function normalizeGraphSpec(spec, _warnings) {
|
|
@@ -8270,7 +8359,7 @@ function computeLegend(spec, strategy, theme, chartArea) {
|
|
|
8270
8359
|
entries = truncateEntries(entries, limit);
|
|
8271
8360
|
}
|
|
8272
8361
|
}
|
|
8273
|
-
const maxRows = spec.legend?.columns != null ? Math.ceil(entries.length / spec.legend.columns) : TOP_LEGEND_MAX_ROWS;
|
|
8362
|
+
const maxRows = spec.legend?.maxRows != null ? Math.max(1, spec.legend.maxRows) : spec.legend?.columns != null ? Math.ceil(entries.length / spec.legend.columns) : TOP_LEGEND_MAX_ROWS;
|
|
8274
8363
|
const maxFit = entriesThatFit(entries, availableWidth, maxRows, labelStyle);
|
|
8275
8364
|
if (maxFit < entries.length) {
|
|
8276
8365
|
entries = truncateEntries(entries, maxFit);
|
|
@@ -8314,6 +8403,7 @@ function computeLegend(spec, strategy, theme, chartArea) {
|
|
|
8314
8403
|
// src/sankey/compile-sankey.ts
|
|
8315
8404
|
import {
|
|
8316
8405
|
adaptTheme as adaptTheme2,
|
|
8406
|
+
buildD3Formatter as buildD3Formatter4,
|
|
8317
8407
|
computeChrome as computeChrome3,
|
|
8318
8408
|
estimateTextWidth as estimateTextWidth10,
|
|
8319
8409
|
formatNumber as formatNumber4,
|
|
@@ -8789,7 +8879,8 @@ var SWATCH_SIZE3 = 12;
|
|
|
8789
8879
|
var SWATCH_GAP3 = 6;
|
|
8790
8880
|
var ENTRY_GAP3 = 16;
|
|
8791
8881
|
var LABEL_GAP = 6;
|
|
8792
|
-
var
|
|
8882
|
+
var LINK_OPACITY_LIGHT = 0.5;
|
|
8883
|
+
var LINK_OPACITY_DARK = 0.75;
|
|
8793
8884
|
var NODE_CORNER_RADIUS = 2;
|
|
8794
8885
|
function pickColor(palette, index) {
|
|
8795
8886
|
return palette[index % palette.length];
|
|
@@ -8874,9 +8965,14 @@ function compileSankey(spec, options) {
|
|
|
8874
8965
|
}
|
|
8875
8966
|
const sankeySpec = normalized;
|
|
8876
8967
|
const mergedThemeConfig = options.theme ? { ...sankeySpec.theme, ...options.theme } : sankeySpec.theme;
|
|
8877
|
-
|
|
8968
|
+
const lightTheme = resolveTheme2(mergedThemeConfig);
|
|
8969
|
+
let theme = lightTheme;
|
|
8878
8970
|
if (options.darkMode) {
|
|
8879
8971
|
theme = adaptTheme2(theme);
|
|
8972
|
+
theme = {
|
|
8973
|
+
...theme,
|
|
8974
|
+
colors: { ...theme.colors, categorical: lightTheme.colors.categorical }
|
|
8975
|
+
};
|
|
8880
8976
|
}
|
|
8881
8977
|
const chrome = computeChrome3(
|
|
8882
8978
|
{
|
|
@@ -8936,17 +9032,53 @@ function compileSankey(spec, options) {
|
|
|
8936
9032
|
if (area.height <= 0) {
|
|
8937
9033
|
return emptyLayout(area, chrome, theme, options);
|
|
8938
9034
|
}
|
|
8939
|
-
const
|
|
9035
|
+
const labelFontSize = theme.fonts.sizes.small;
|
|
9036
|
+
const labelFontWeight = theme.fonts.weights.normal;
|
|
9037
|
+
const nodeWidth = sankeySpec.nodeWidth ?? 12;
|
|
9038
|
+
let layoutArea = { ...area };
|
|
9039
|
+
let { nodes, links } = computeSankeyLayout(
|
|
8940
9040
|
sankeySpec.data,
|
|
8941
9041
|
sourceField,
|
|
8942
9042
|
targetField,
|
|
8943
9043
|
valueField,
|
|
8944
|
-
|
|
9044
|
+
layoutArea,
|
|
8945
9045
|
sankeySpec.nodeWidth,
|
|
8946
9046
|
sankeySpec.nodePadding,
|
|
8947
9047
|
sankeySpec.nodeAlign,
|
|
8948
9048
|
sankeySpec.iterations
|
|
8949
9049
|
);
|
|
9050
|
+
const maxDepthFirst = nodes.reduce((max4, n) => Math.max(max4, n.depth ?? 0), 0);
|
|
9051
|
+
const rightEdge = area.x + area.width;
|
|
9052
|
+
let maxOverflow = 0;
|
|
9053
|
+
for (const node of nodes) {
|
|
9054
|
+
const depth = node.depth ?? 0;
|
|
9055
|
+
if (depth === maxDepthFirst) continue;
|
|
9056
|
+
const labelX = (node.x1 ?? nodeWidth) + LABEL_GAP;
|
|
9057
|
+
const labelText = node.label ?? node.id;
|
|
9058
|
+
const labelWidth = estimateTextWidth10(labelText, labelFontSize, labelFontWeight);
|
|
9059
|
+
const overflow = labelX + labelWidth - rightEdge;
|
|
9060
|
+
if (overflow > maxOverflow) maxOverflow = overflow;
|
|
9061
|
+
}
|
|
9062
|
+
if (maxOverflow > 0) {
|
|
9063
|
+
const margin = Math.ceil(maxOverflow) + 4;
|
|
9064
|
+
layoutArea = {
|
|
9065
|
+
x: area.x,
|
|
9066
|
+
y: area.y,
|
|
9067
|
+
width: Math.max(area.width - margin, 40),
|
|
9068
|
+
height: area.height
|
|
9069
|
+
};
|
|
9070
|
+
({ nodes, links } = computeSankeyLayout(
|
|
9071
|
+
sankeySpec.data,
|
|
9072
|
+
sourceField,
|
|
9073
|
+
targetField,
|
|
9074
|
+
valueField,
|
|
9075
|
+
layoutArea,
|
|
9076
|
+
sankeySpec.nodeWidth,
|
|
9077
|
+
sankeySpec.nodePadding,
|
|
9078
|
+
sankeySpec.nodeAlign,
|
|
9079
|
+
sankeySpec.iterations
|
|
9080
|
+
));
|
|
9081
|
+
}
|
|
8950
9082
|
const nodeColorMap = buildNodeColorMap(
|
|
8951
9083
|
nodes,
|
|
8952
9084
|
theme.colors.categorical,
|
|
@@ -8974,7 +9106,7 @@ function compileSankey(spec, options) {
|
|
|
8974
9106
|
data: { id: node.id, label: node.label },
|
|
8975
9107
|
aria: {
|
|
8976
9108
|
role: "img",
|
|
8977
|
-
label: `${node.label}: ${
|
|
9109
|
+
label: `${node.label}: ${formatFlowValue(node.value ?? 0, sankeySpec.valueFormat)}`
|
|
8978
9110
|
},
|
|
8979
9111
|
animationIndex: 0
|
|
8980
9112
|
// Reassigned below after sorting by depth
|
|
@@ -8996,7 +9128,7 @@ function compileSankey(spec, options) {
|
|
|
8996
9128
|
path: generateLinkPath(link),
|
|
8997
9129
|
sourceColor: colors.sourceColor,
|
|
8998
9130
|
targetColor: colors.targetColor,
|
|
8999
|
-
fillOpacity:
|
|
9131
|
+
fillOpacity: sankeySpec.linkOpacity ?? (options.darkMode ? LINK_OPACITY_DARK : LINK_OPACITY_LIGHT),
|
|
9000
9132
|
sourceId: sourceNode.id,
|
|
9001
9133
|
targetId: targetNode.id,
|
|
9002
9134
|
width: link.width ?? 0,
|
|
@@ -9004,7 +9136,7 @@ function compileSankey(spec, options) {
|
|
|
9004
9136
|
data: link.data ?? {},
|
|
9005
9137
|
aria: {
|
|
9006
9138
|
role: "img",
|
|
9007
|
-
label: `${sourceNode.label} to ${targetNode.label}: ${
|
|
9139
|
+
label: `${sourceNode.label} to ${targetNode.label}: ${formatFlowValue(link.value, sankeySpec.valueFormat)}`
|
|
9008
9140
|
},
|
|
9009
9141
|
// Links animate after nodes
|
|
9010
9142
|
animationIndex: nodeMarks.length + i
|
|
@@ -9019,7 +9151,7 @@ function compileSankey(spec, options) {
|
|
|
9019
9151
|
theme,
|
|
9020
9152
|
fullArea
|
|
9021
9153
|
);
|
|
9022
|
-
const tooltipDescriptors = buildTooltipDescriptors(nodeMarks, linkMarks);
|
|
9154
|
+
const tooltipDescriptors = buildTooltipDescriptors(nodeMarks, linkMarks, sankeySpec.valueFormat);
|
|
9023
9155
|
const a11y = {
|
|
9024
9156
|
altText: `Sankey diagram with ${nodeMarks.length} nodes and ${linkMarks.length} links`,
|
|
9025
9157
|
dataTableFallback: linkMarks.map((l) => [l.sourceId, l.targetId, String(l.value)]),
|
|
@@ -9111,13 +9243,20 @@ function buildSankeyLegend(nodeColorMap, colorField, data, sourceField, targetFi
|
|
|
9111
9243
|
entryGap: ENTRY_GAP3
|
|
9112
9244
|
};
|
|
9113
9245
|
}
|
|
9114
|
-
function
|
|
9246
|
+
function formatFlowValue(value2, valueFormat) {
|
|
9247
|
+
if (valueFormat) {
|
|
9248
|
+
const fmt = buildD3Formatter4(valueFormat);
|
|
9249
|
+
if (fmt) return fmt(value2);
|
|
9250
|
+
}
|
|
9251
|
+
return formatNumber4(value2);
|
|
9252
|
+
}
|
|
9253
|
+
function buildTooltipDescriptors(nodes, links, valueFormat) {
|
|
9115
9254
|
const descriptors = /* @__PURE__ */ new Map();
|
|
9116
9255
|
for (const node of nodes) {
|
|
9117
9256
|
const fields = [
|
|
9118
9257
|
{
|
|
9119
9258
|
label: "Total flow",
|
|
9120
|
-
value:
|
|
9259
|
+
value: formatFlowValue(node.value, valueFormat)
|
|
9121
9260
|
}
|
|
9122
9261
|
];
|
|
9123
9262
|
descriptors.set(`node-${node.nodeId}`, {
|
|
@@ -9125,14 +9264,15 @@ function buildTooltipDescriptors(nodes, links) {
|
|
|
9125
9264
|
fields
|
|
9126
9265
|
});
|
|
9127
9266
|
}
|
|
9128
|
-
for (
|
|
9267
|
+
for (let i = 0; i < links.length; i++) {
|
|
9268
|
+
const link = links[i];
|
|
9129
9269
|
const fields = [
|
|
9130
9270
|
{
|
|
9131
9271
|
label: "Flow",
|
|
9132
|
-
value:
|
|
9272
|
+
value: formatFlowValue(link.value, valueFormat)
|
|
9133
9273
|
}
|
|
9134
9274
|
];
|
|
9135
|
-
descriptors.set(`link-${link.sourceId}-${link.targetId}`, {
|
|
9275
|
+
descriptors.set(`link-${link.sourceId}-${link.targetId}-${i}`, {
|
|
9136
9276
|
title: `${link.sourceId} \u2192 ${link.targetId}`,
|
|
9137
9277
|
fields
|
|
9138
9278
|
});
|
|
@@ -9284,7 +9424,7 @@ function computeCategoryColors(data, column, theme, darkMode) {
|
|
|
9284
9424
|
}
|
|
9285
9425
|
|
|
9286
9426
|
// src/tables/format-cells.ts
|
|
9287
|
-
import { formatDate as formatDate2, formatNumber as formatNumber5 } from "@opendata-ai/openchart-core";
|
|
9427
|
+
import { buildD3Formatter as buildD3Formatter5, formatDate as formatDate2, formatNumber as formatNumber5 } from "@opendata-ai/openchart-core";
|
|
9288
9428
|
function isNumericValue(value2) {
|
|
9289
9429
|
if (typeof value2 === "number") return Number.isFinite(value2);
|
|
9290
9430
|
return false;
|
|
@@ -9303,14 +9443,13 @@ function formatCell(value2, column) {
|
|
|
9303
9443
|
};
|
|
9304
9444
|
}
|
|
9305
9445
|
if (column.format && isNumericValue(value2)) {
|
|
9306
|
-
|
|
9307
|
-
|
|
9446
|
+
const formatter = buildD3Formatter5(column.format);
|
|
9447
|
+
if (formatter) {
|
|
9308
9448
|
return {
|
|
9309
9449
|
value: value2,
|
|
9310
9450
|
formattedValue: formatter(value2),
|
|
9311
9451
|
style
|
|
9312
9452
|
};
|
|
9313
|
-
} catch {
|
|
9314
9453
|
}
|
|
9315
9454
|
}
|
|
9316
9455
|
if (isNumericValue(value2)) {
|
|
@@ -9336,9 +9475,9 @@ function formatCell(value2, column) {
|
|
|
9336
9475
|
function formatValueForSearch(value2, column) {
|
|
9337
9476
|
if (value2 == null) return "";
|
|
9338
9477
|
if (column.format && isNumericValue(value2)) {
|
|
9339
|
-
|
|
9340
|
-
|
|
9341
|
-
|
|
9478
|
+
const formatter = buildD3Formatter5(column.format);
|
|
9479
|
+
if (formatter) {
|
|
9480
|
+
return formatter(value2);
|
|
9342
9481
|
}
|
|
9343
9482
|
}
|
|
9344
9483
|
if (isNumericValue(value2)) {
|