@opendata-ai/openchart-engine 2.4.0 → 2.6.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 +76 -21
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/__test-fixtures__/specs.ts +3 -0
- package/src/annotations/compute.ts +24 -5
- package/src/charts/bar/index.ts +1 -1
- package/src/charts/bar/labels.ts +33 -2
- package/src/charts/line/__tests__/compute.test.ts +109 -0
- package/src/charts/line/compute.ts +18 -6
- package/src/compiler/normalize.ts +1 -0
- package/src/compiler/types.ts +2 -0
- package/src/layout/axes.ts +16 -1
- package/src/layout/dimensions.ts +4 -2
package/dist/index.d.ts
CHANGED
|
@@ -180,6 +180,8 @@ interface NormalizedChartSpec {
|
|
|
180
180
|
darkMode: DarkMode;
|
|
181
181
|
/** Series names to hide from rendering. */
|
|
182
182
|
hiddenSeries: string[];
|
|
183
|
+
/** Per-series visual style overrides. */
|
|
184
|
+
seriesStyles: Record<string, _opendata_ai_openchart_core.SeriesStyle>;
|
|
183
185
|
}
|
|
184
186
|
/** A TableSpec with all optional fields filled with sensible defaults. */
|
|
185
187
|
interface NormalizedTableSpec {
|
package/dist/index.js
CHANGED
|
@@ -202,14 +202,23 @@ function resolveRangeAnnotation(annotation, scales, chartArea, isDark) {
|
|
|
202
202
|
const rect = { x, y, width, height };
|
|
203
203
|
let label;
|
|
204
204
|
if (annotation.label) {
|
|
205
|
-
const
|
|
205
|
+
const anchor = annotation.labelAnchor ?? "left";
|
|
206
|
+
const centered = anchor === "top" || anchor === "bottom" || anchor === "auto";
|
|
207
|
+
const baseDx = centered ? 0 : anchor === "right" ? -4 : 4;
|
|
206
208
|
const baseDy = 14;
|
|
207
209
|
const labelDelta = applyOffset({ dx: baseDx, dy: baseDy }, annotation.labelOffset);
|
|
210
|
+
const style = makeAnnotationLabelStyle(11, 500, void 0, isDark);
|
|
211
|
+
if (centered) {
|
|
212
|
+
style.textAnchor = "middle";
|
|
213
|
+
} else if (anchor === "right") {
|
|
214
|
+
style.textAnchor = "end";
|
|
215
|
+
}
|
|
216
|
+
const baseX = centered ? x + width / 2 : anchor === "right" ? x + width : x;
|
|
208
217
|
label = {
|
|
209
218
|
text: annotation.label,
|
|
210
|
-
x:
|
|
219
|
+
x: baseX + labelDelta.dx,
|
|
211
220
|
y: y + labelDelta.dy,
|
|
212
|
-
style
|
|
221
|
+
style,
|
|
213
222
|
visible: true
|
|
214
223
|
};
|
|
215
224
|
}
|
|
@@ -249,8 +258,9 @@ function resolveRefLineAnnotation(annotation, scales, chartArea, isDark) {
|
|
|
249
258
|
let label;
|
|
250
259
|
if (annotation.label) {
|
|
251
260
|
const isHorizontal = annotation.y !== void 0;
|
|
261
|
+
const anchor = annotation.labelAnchor ?? "top";
|
|
252
262
|
const baseDx = isHorizontal ? -4 : 4;
|
|
253
|
-
const baseDy = -4;
|
|
263
|
+
const baseDy = anchor === "bottom" ? 14 : -4;
|
|
254
264
|
const labelDelta = applyOffset({ dx: baseDx, dy: baseDy }, annotation.labelOffset);
|
|
255
265
|
const defaultStroke2 = isDark ? DARK_REFLINE_STROKE : LIGHT_REFLINE_STROKE;
|
|
256
266
|
const style = makeAnnotationLabelStyle(11, 400, annotation.stroke ?? defaultStroke2, isDark);
|
|
@@ -580,19 +590,41 @@ function computeSimpleBars(data, valueField, categoryField, xScale, yScale, band
|
|
|
580
590
|
|
|
581
591
|
// src/charts/bar/labels.ts
|
|
582
592
|
import { estimateTextWidth as estimateTextWidth2, resolveCollisions } from "@opendata-ai/openchart-core";
|
|
593
|
+
import { format as d3Format } from "d3-format";
|
|
583
594
|
var LABEL_FONT_SIZE = 11;
|
|
584
595
|
var LABEL_FONT_WEIGHT = 600;
|
|
585
596
|
var LABEL_PADDING = 6;
|
|
586
597
|
var MIN_WIDTH_FOR_INSIDE_LABEL = 40;
|
|
587
|
-
function computeBarLabels(marks, _chartArea, density = "auto") {
|
|
598
|
+
function computeBarLabels(marks, _chartArea, density = "auto", labelFormat) {
|
|
588
599
|
if (density === "none") return [];
|
|
589
600
|
const targetMarks = density === "endpoints" && marks.length > 1 ? [marks[0], marks[marks.length - 1]] : marks;
|
|
590
601
|
const candidates = [];
|
|
602
|
+
let formatter = null;
|
|
603
|
+
if (labelFormat) {
|
|
604
|
+
try {
|
|
605
|
+
formatter = d3Format(labelFormat);
|
|
606
|
+
} catch {
|
|
607
|
+
const suffixMatch = labelFormat.match(/^(.+[a-z])([^a-z]+)$/i);
|
|
608
|
+
if (suffixMatch) {
|
|
609
|
+
try {
|
|
610
|
+
const d3Fmt = d3Format(suffixMatch[1]);
|
|
611
|
+
const suffix = suffixMatch[2];
|
|
612
|
+
formatter = (v) => d3Fmt(v) + suffix;
|
|
613
|
+
} catch {
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
}
|
|
591
618
|
for (const mark of targetMarks) {
|
|
592
619
|
const ariaLabel = mark.aria.label;
|
|
593
620
|
const lastColon = ariaLabel.lastIndexOf(":");
|
|
594
|
-
const
|
|
595
|
-
if (!
|
|
621
|
+
const rawValue = lastColon >= 0 ? ariaLabel.slice(lastColon + 1).trim() : "";
|
|
622
|
+
if (!rawValue) continue;
|
|
623
|
+
let valuePart = rawValue;
|
|
624
|
+
if (formatter) {
|
|
625
|
+
const num = Number(rawValue.replace(/[^0-9.-]/g, ""));
|
|
626
|
+
if (!Number.isNaN(num)) valuePart = formatter(num);
|
|
627
|
+
}
|
|
596
628
|
const textWidth = estimateTextWidth2(valuePart, LABEL_FONT_SIZE, LABEL_FONT_WEIGHT);
|
|
597
629
|
const textHeight = LABEL_FONT_SIZE * 1.2;
|
|
598
630
|
const isStacked = mark.cornerRadius === 0;
|
|
@@ -648,7 +680,7 @@ function computeBarLabels(marks, _chartArea, density = "auto") {
|
|
|
648
680
|
// src/charts/bar/index.ts
|
|
649
681
|
var barRenderer = (spec, scales, chartArea, strategy, _theme) => {
|
|
650
682
|
const marks = computeBarMarks(spec, scales, chartArea, strategy);
|
|
651
|
-
const labels = computeBarLabels(marks, chartArea, spec.labels.density);
|
|
683
|
+
const labels = computeBarLabels(marks, chartArea, spec.labels.density, spec.labels.format);
|
|
652
684
|
for (let i = 0; i < marks.length && i < labels.length; i++) {
|
|
653
685
|
marks[i].label = labels[i];
|
|
654
686
|
}
|
|
@@ -1299,27 +1331,35 @@ function computeLineMarks(spec, scales, _chartArea, _strategy) {
|
|
|
1299
1331
|
label: ariaLabel
|
|
1300
1332
|
};
|
|
1301
1333
|
const combinedPath = pathParts.join(" ");
|
|
1334
|
+
const seriesStyleKey = seriesKey === "__default__" ? void 0 : seriesKey;
|
|
1335
|
+
const styleOverride = seriesStyleKey ? spec.seriesStyles?.[seriesStyleKey] : void 0;
|
|
1336
|
+
let strokeDasharray;
|
|
1337
|
+
if (styleOverride?.lineStyle === "dashed") strokeDasharray = "6 4";
|
|
1338
|
+
else if (styleOverride?.lineStyle === "dotted") strokeDasharray = "2 3";
|
|
1302
1339
|
const lineMark = {
|
|
1303
1340
|
type: "line",
|
|
1304
1341
|
points: allPoints,
|
|
1305
1342
|
path: combinedPath,
|
|
1306
1343
|
stroke: color,
|
|
1307
|
-
strokeWidth: DEFAULT_STROKE_WIDTH,
|
|
1308
|
-
|
|
1344
|
+
strokeWidth: styleOverride?.strokeWidth ?? DEFAULT_STROKE_WIDTH,
|
|
1345
|
+
strokeDasharray,
|
|
1346
|
+
opacity: styleOverride?.opacity,
|
|
1347
|
+
seriesKey: seriesStyleKey,
|
|
1309
1348
|
data: pointsWithData.map((p) => p.row),
|
|
1310
1349
|
aria
|
|
1311
1350
|
};
|
|
1312
1351
|
marks.push(lineMark);
|
|
1352
|
+
const showPoints = styleOverride?.showPoints !== false;
|
|
1313
1353
|
for (let i = 0; i < pointsWithData.length; i++) {
|
|
1314
1354
|
const p = pointsWithData[i];
|
|
1315
1355
|
const pointMark = {
|
|
1316
1356
|
type: "point",
|
|
1317
1357
|
cx: p.x,
|
|
1318
1358
|
cy: p.y,
|
|
1319
|
-
r: DEFAULT_POINT_RADIUS,
|
|
1359
|
+
r: showPoints ? DEFAULT_POINT_RADIUS : 0,
|
|
1320
1360
|
fill: color,
|
|
1321
|
-
stroke: "#ffffff",
|
|
1322
|
-
strokeWidth: 1.5,
|
|
1361
|
+
stroke: showPoints ? "#ffffff" : "transparent",
|
|
1362
|
+
strokeWidth: showPoints ? 1.5 : 0,
|
|
1323
1363
|
fillOpacity: 0,
|
|
1324
1364
|
data: p.row,
|
|
1325
1365
|
aria: {
|
|
@@ -1899,7 +1939,8 @@ function normalizeChartSpec(spec, warnings) {
|
|
|
1899
1939
|
responsive: spec.responsive ?? true,
|
|
1900
1940
|
theme: spec.theme ?? {},
|
|
1901
1941
|
darkMode: spec.darkMode ?? "off",
|
|
1902
|
-
hiddenSeries: spec.hiddenSeries ?? []
|
|
1942
|
+
hiddenSeries: spec.hiddenSeries ?? [],
|
|
1943
|
+
seriesStyles: spec.seriesStyles ?? {}
|
|
1903
1944
|
};
|
|
1904
1945
|
}
|
|
1905
1946
|
function normalizeTableSpec(spec, _warnings) {
|
|
@@ -2796,6 +2837,7 @@ var DEFAULT_COLLISION_PADDING = 5;
|
|
|
2796
2837
|
|
|
2797
2838
|
// src/layout/axes.ts
|
|
2798
2839
|
import { abbreviateNumber as abbreviateNumber3, formatDate, formatNumber as formatNumber3 } from "@opendata-ai/openchart-core";
|
|
2840
|
+
import { format as d3Format2 } from "d3-format";
|
|
2799
2841
|
var TICK_COUNTS = {
|
|
2800
2842
|
full: 8,
|
|
2801
2843
|
reduced: 5,
|
|
@@ -2855,7 +2897,19 @@ function formatTickLabel(value, resolvedScale) {
|
|
|
2855
2897
|
}
|
|
2856
2898
|
if (resolvedScale.type === "linear" || resolvedScale.type === "log") {
|
|
2857
2899
|
const num = value;
|
|
2858
|
-
if (formatStr)
|
|
2900
|
+
if (formatStr) {
|
|
2901
|
+
try {
|
|
2902
|
+
return d3Format2(formatStr)(num);
|
|
2903
|
+
} catch {
|
|
2904
|
+
const suffixMatch = formatStr.match(/^(.+[a-z])([^a-z]+)$/i);
|
|
2905
|
+
if (suffixMatch) {
|
|
2906
|
+
try {
|
|
2907
|
+
return d3Format2(suffixMatch[1])(num) + suffixMatch[2];
|
|
2908
|
+
} catch {
|
|
2909
|
+
}
|
|
2910
|
+
}
|
|
2911
|
+
}
|
|
2912
|
+
}
|
|
2859
2913
|
if (Math.abs(num) >= 1e3) return abbreviateNumber3(num);
|
|
2860
2914
|
return formatNumber3(num);
|
|
2861
2915
|
}
|
|
@@ -2977,7 +3031,8 @@ function computeDimensions(spec, options, legendLayout, theme) {
|
|
|
2977
3031
|
bottom: padding + chrome.bottomHeight + xAxisHeight,
|
|
2978
3032
|
left: padding + (isRadial ? padding : axisMargin)
|
|
2979
3033
|
};
|
|
2980
|
-
|
|
3034
|
+
const labelDensity = spec.labels.density;
|
|
3035
|
+
if ((spec.type === "line" || spec.type === "area") && labelDensity !== "none") {
|
|
2981
3036
|
const colorField = encoding.color?.field;
|
|
2982
3037
|
if (colorField) {
|
|
2983
3038
|
let maxLabelWidth = 0;
|
|
@@ -3508,7 +3563,7 @@ function computeCategoryColors(data, column, theme, darkMode) {
|
|
|
3508
3563
|
|
|
3509
3564
|
// src/tables/format-cells.ts
|
|
3510
3565
|
import { formatDate as formatDate2, formatNumber as formatNumber4 } from "@opendata-ai/openchart-core";
|
|
3511
|
-
import { format as
|
|
3566
|
+
import { format as d3Format3 } from "d3-format";
|
|
3512
3567
|
function isNumericValue(value) {
|
|
3513
3568
|
if (typeof value === "number") return Number.isFinite(value);
|
|
3514
3569
|
return false;
|
|
@@ -3528,7 +3583,7 @@ function formatCell(value, column) {
|
|
|
3528
3583
|
}
|
|
3529
3584
|
if (column.format && isNumericValue(value)) {
|
|
3530
3585
|
try {
|
|
3531
|
-
const formatter =
|
|
3586
|
+
const formatter = d3Format3(column.format);
|
|
3532
3587
|
return {
|
|
3533
3588
|
value,
|
|
3534
3589
|
formattedValue: formatter(value),
|
|
@@ -3561,7 +3616,7 @@ function formatValueForSearch(value, column) {
|
|
|
3561
3616
|
if (value == null) return "";
|
|
3562
3617
|
if (column.format && isNumericValue(value)) {
|
|
3563
3618
|
try {
|
|
3564
|
-
return
|
|
3619
|
+
return d3Format3(column.format)(value);
|
|
3565
3620
|
} catch {
|
|
3566
3621
|
}
|
|
3567
3622
|
}
|
|
@@ -4035,7 +4090,7 @@ function compileTableLayout(spec, options, theme) {
|
|
|
4035
4090
|
|
|
4036
4091
|
// src/tooltips/compute.ts
|
|
4037
4092
|
import { formatDate as formatDate3, formatNumber as formatNumber5 } from "@opendata-ai/openchart-core";
|
|
4038
|
-
import { format as
|
|
4093
|
+
import { format as d3Format4 } from "d3-format";
|
|
4039
4094
|
function formatValue(value, fieldType, format) {
|
|
4040
4095
|
if (value == null) return "";
|
|
4041
4096
|
if (fieldType === "temporal" || value instanceof Date) {
|
|
@@ -4044,7 +4099,7 @@ function formatValue(value, fieldType, format) {
|
|
|
4044
4099
|
if (typeof value === "number") {
|
|
4045
4100
|
if (format) {
|
|
4046
4101
|
try {
|
|
4047
|
-
return
|
|
4102
|
+
return d3Format4(format)(value);
|
|
4048
4103
|
} catch {
|
|
4049
4104
|
return formatNumber5(value);
|
|
4050
4105
|
}
|