@opendata-ai/openchart-engine 7.1.4 → 7.2.1
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 +1 -1
- package/dist/index.js +63 -21
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/__snapshots__/compile-snapshot.test.ts.snap +48 -42
- package/src/__tests__/compile-chart.test.ts +27 -0
- package/src/__tests__/scales.test.ts +34 -0
- package/src/charts/bar/index.ts +2 -0
- package/src/charts/bar/labels.ts +7 -3
- package/src/charts/column/index.ts +2 -0
- package/src/charts/column/labels.ts +9 -5
- package/src/charts/line/index.ts +53 -3
- package/src/compiler/normalize.ts +2 -0
- package/src/compiler/types.ts +1 -1
- package/src/layout/axes/ticks.ts +7 -6
- package/src/layout/dimensions.ts +2 -1
- package/src/layout/scales.ts +5 -1
package/dist/index.d.ts
CHANGED
|
@@ -298,7 +298,7 @@ interface NormalizedChartSpec {
|
|
|
298
298
|
metrics?: _opendata_ai_openchart_core.Metric[];
|
|
299
299
|
annotations: Annotation[];
|
|
300
300
|
/** Normalized label configuration with defaults applied. density, format, and prefix are always set; offsets and color stay optional. */
|
|
301
|
-
labels: Required<Pick<LabelConfig, 'density' | 'format' | 'prefix'>> & Pick<LabelConfig, 'offsets' | 'color'>;
|
|
301
|
+
labels: Required<Pick<LabelConfig, 'density' | 'format' | 'prefix'>> & Pick<LabelConfig, 'offsets' | 'color' | 'fontSize' | 'suffix'>;
|
|
302
302
|
/** Legend configuration (position override). */
|
|
303
303
|
legend?: LegendConfig;
|
|
304
304
|
/** Right-side endpoint labels column config (multi-series line/area only). */
|
package/dist/index.js
CHANGED
|
@@ -1778,7 +1778,8 @@ var LABEL_FONT_SIZE = 11;
|
|
|
1778
1778
|
var LABEL_FONT_WEIGHT = 600;
|
|
1779
1779
|
var LABEL_PADDING = 6;
|
|
1780
1780
|
var MIN_WIDTH_FOR_INSIDE_LABEL = 40;
|
|
1781
|
-
function computeBarLabels(marks, _chartArea, density = "auto", labelFormat, labelPrefix, valueField, labelColor, darkMode = false) {
|
|
1781
|
+
function computeBarLabels(marks, _chartArea, density = "auto", labelFormat, labelPrefix, valueField, labelColor, darkMode = false, fontSize, labelSuffix) {
|
|
1782
|
+
const FONT_SIZE = fontSize ?? LABEL_FONT_SIZE;
|
|
1782
1783
|
const targetMarks = filterByDensity(marks, density);
|
|
1783
1784
|
const candidates = [];
|
|
1784
1785
|
const fitsInSegment = [];
|
|
@@ -1804,8 +1805,9 @@ function computeBarLabels(marks, _chartArea, density = "auto", labelFormat, labe
|
|
|
1804
1805
|
}
|
|
1805
1806
|
}
|
|
1806
1807
|
if (labelPrefix) valuePart = labelPrefix + valuePart;
|
|
1807
|
-
|
|
1808
|
-
const
|
|
1808
|
+
if (labelSuffix) valuePart = valuePart + labelSuffix;
|
|
1809
|
+
const textWidth = estimateTextWidth4(valuePart, FONT_SIZE, LABEL_FONT_WEIGHT);
|
|
1810
|
+
const textHeight = FONT_SIZE * 1.2;
|
|
1809
1811
|
const isStacked2 = mark.stackGroup !== void 0;
|
|
1810
1812
|
const isInside = mark.width >= MIN_WIDTH_FOR_INSIDE_LABEL;
|
|
1811
1813
|
const isNegative = Number.isFinite(rawNum) ? rawNum < 0 : false;
|
|
@@ -1850,7 +1852,7 @@ function computeBarLabels(marks, _chartArea, density = "auto", labelFormat, labe
|
|
|
1850
1852
|
priority: "data",
|
|
1851
1853
|
style: {
|
|
1852
1854
|
fontFamily: "system-ui, -apple-system, sans-serif",
|
|
1853
|
-
fontSize:
|
|
1855
|
+
fontSize: FONT_SIZE,
|
|
1854
1856
|
fontWeight: LABEL_FONT_WEIGHT,
|
|
1855
1857
|
fill,
|
|
1856
1858
|
lineHeight: 1.2,
|
|
@@ -1909,7 +1911,9 @@ var barRenderer = (spec, scales, chartArea, strategy, theme) => {
|
|
|
1909
1911
|
spec.labels.prefix,
|
|
1910
1912
|
valueField,
|
|
1911
1913
|
spec.labels.color,
|
|
1912
|
-
theme.isDark
|
|
1914
|
+
theme.isDark,
|
|
1915
|
+
spec.labels.fontSize,
|
|
1916
|
+
spec.labels.suffix
|
|
1913
1917
|
);
|
|
1914
1918
|
for (let i = 0; i < marks.length && i < labels.length; i++) {
|
|
1915
1919
|
marks[i].label = labels[i];
|
|
@@ -2214,7 +2218,8 @@ import {
|
|
|
2214
2218
|
var LABEL_FONT_SIZE2 = 10;
|
|
2215
2219
|
var LABEL_FONT_WEIGHT2 = 600;
|
|
2216
2220
|
var LABEL_OFFSET_Y = 8;
|
|
2217
|
-
function computeColumnLabels(marks, _chartArea, density = "auto", labelFormat, labelPrefix, valueField, labelColor) {
|
|
2221
|
+
function computeColumnLabels(marks, _chartArea, density = "auto", labelFormat, labelPrefix, valueField, labelColor, fontSize, labelSuffix) {
|
|
2222
|
+
const FONT_SIZE = fontSize ?? LABEL_FONT_SIZE2;
|
|
2218
2223
|
const targetMarks = filterByDensity(marks, density);
|
|
2219
2224
|
const formatter = buildD3Formatter2(labelFormat);
|
|
2220
2225
|
const candidates = [];
|
|
@@ -2238,11 +2243,12 @@ function computeColumnLabels(marks, _chartArea, density = "auto", labelFormat, l
|
|
|
2238
2243
|
valuePart = rawValue;
|
|
2239
2244
|
}
|
|
2240
2245
|
}
|
|
2241
|
-
if (labelPrefix) valuePart = labelPrefix + valuePart;
|
|
2242
2246
|
const numericValue = parseFloat(valuePart);
|
|
2243
2247
|
const isNegative = Number.isFinite(numericValue) && numericValue < 0;
|
|
2244
|
-
|
|
2245
|
-
|
|
2248
|
+
if (labelPrefix) valuePart = labelPrefix + valuePart;
|
|
2249
|
+
if (labelSuffix) valuePart = valuePart + labelSuffix;
|
|
2250
|
+
const textWidth = estimateTextWidth5(valuePart, FONT_SIZE, LABEL_FONT_WEIGHT2);
|
|
2251
|
+
const textHeight = FONT_SIZE * 1.2;
|
|
2246
2252
|
const anchorX = mark.x + mark.width / 2;
|
|
2247
2253
|
const anchorY = isNegative ? mark.y + mark.height + LABEL_OFFSET_Y : mark.y - LABEL_OFFSET_Y - textHeight;
|
|
2248
2254
|
candidates.push({
|
|
@@ -2254,7 +2260,7 @@ function computeColumnLabels(marks, _chartArea, density = "auto", labelFormat, l
|
|
|
2254
2260
|
priority: "data",
|
|
2255
2261
|
style: {
|
|
2256
2262
|
fontFamily: "system-ui, -apple-system, sans-serif",
|
|
2257
|
-
fontSize:
|
|
2263
|
+
fontSize: FONT_SIZE,
|
|
2258
2264
|
fontWeight: LABEL_FONT_WEIGHT2,
|
|
2259
2265
|
fill: labelColor ?? getRepresentativeColor2(mark.fill),
|
|
2260
2266
|
lineHeight: 1.2,
|
|
@@ -2287,7 +2293,9 @@ var columnRenderer = (spec, scales, chartArea, strategy, _theme) => {
|
|
|
2287
2293
|
spec.labels.format,
|
|
2288
2294
|
spec.labels.prefix,
|
|
2289
2295
|
valueField,
|
|
2290
|
-
spec.labels.color
|
|
2296
|
+
spec.labels.color,
|
|
2297
|
+
spec.labels.fontSize,
|
|
2298
|
+
spec.labels.suffix
|
|
2291
2299
|
);
|
|
2292
2300
|
for (let i = 0; i < marks.length && i < labels.length; i++) {
|
|
2293
2301
|
marks[i].label = labels[i];
|
|
@@ -3924,6 +3932,7 @@ function computeLineLabels(marks, strategy, density = "auto", labelOffsets, spec
|
|
|
3924
3932
|
}
|
|
3925
3933
|
|
|
3926
3934
|
// src/charts/line/index.ts
|
|
3935
|
+
var AREA_POINT_RADIUS = 3;
|
|
3927
3936
|
var lineRenderer = (spec, scales, chartArea, strategy, _theme) => {
|
|
3928
3937
|
const marks = computeLineMarks(spec, scales, chartArea, strategy);
|
|
3929
3938
|
const lineMarks = marks.filter((m) => m.type === "line");
|
|
@@ -3949,7 +3958,8 @@ var areaRenderer = (spec, scales, chartArea, strategy, theme) => {
|
|
|
3949
3958
|
const encoding = spec.encoding;
|
|
3950
3959
|
const hasColor = !!(encoding.color && "field" in encoding.color);
|
|
3951
3960
|
const lines = hasColor ? linesFromAreas(areas) : computeLineMarks(spec, scales, chartArea, strategy);
|
|
3952
|
-
|
|
3961
|
+
const points = hasColor && spec.markDef.point ? pointsFromAreas(areas, spec.markDef.point) : [];
|
|
3962
|
+
return [...areas, ...lines, ...points];
|
|
3953
3963
|
};
|
|
3954
3964
|
function linesFromAreas(areas) {
|
|
3955
3965
|
return areas.map((a) => ({
|
|
@@ -3964,6 +3974,34 @@ function linesFromAreas(areas) {
|
|
|
3964
3974
|
aria: { label: `${a.seriesKey ?? "Series"}: line with ${a.topPoints.length} data points` }
|
|
3965
3975
|
}));
|
|
3966
3976
|
}
|
|
3977
|
+
function pointsFromAreas(areas, pointMode) {
|
|
3978
|
+
const isTransparent = pointMode === "transparent";
|
|
3979
|
+
const isEndpoints = pointMode === "endpoints";
|
|
3980
|
+
const points = [];
|
|
3981
|
+
for (const a of areas) {
|
|
3982
|
+
const stroke = getRepresentativeColor6(a.fill);
|
|
3983
|
+
const lastIdx = a.topPoints.length - 1;
|
|
3984
|
+
for (let i = 0; i < a.topPoints.length; i++) {
|
|
3985
|
+
const pt = a.topPoints[i];
|
|
3986
|
+
const isEndpoint = i === 0 || i === lastIdx;
|
|
3987
|
+
const visible = !isTransparent && (!isEndpoints || isEndpoint);
|
|
3988
|
+
const hollow = isEndpoints && visible;
|
|
3989
|
+
points.push({
|
|
3990
|
+
type: "point",
|
|
3991
|
+
cx: pt.x,
|
|
3992
|
+
cy: pt.y,
|
|
3993
|
+
r: visible ? AREA_POINT_RADIUS : 0,
|
|
3994
|
+
fill: hollow ? "transparent" : stroke,
|
|
3995
|
+
stroke: hollow ? stroke : visible ? "#ffffff" : "transparent",
|
|
3996
|
+
strokeWidth: visible ? 1.5 : 0,
|
|
3997
|
+
fillOpacity: isTransparent ? 0 : 1,
|
|
3998
|
+
data: a.data[i] ?? {},
|
|
3999
|
+
aria: { decorative: true }
|
|
4000
|
+
});
|
|
4001
|
+
}
|
|
4002
|
+
}
|
|
4003
|
+
return points;
|
|
4004
|
+
}
|
|
3967
4005
|
|
|
3968
4006
|
// src/charts/pie/compute.ts
|
|
3969
4007
|
import { isConditionalDef, isGradientDef as isGradientDef4 } from "@opendata-ai/openchart-core";
|
|
@@ -7287,8 +7325,10 @@ function normalizeLabels(labels) {
|
|
|
7287
7325
|
density: labels.density ?? "auto",
|
|
7288
7326
|
format: labels.format ?? "",
|
|
7289
7327
|
prefix: labels.prefix ?? "",
|
|
7328
|
+
suffix: labels.suffix,
|
|
7290
7329
|
offsets: labels.offsets,
|
|
7291
|
-
color: labels.color
|
|
7330
|
+
color: labels.color,
|
|
7331
|
+
fontSize: labels.fontSize
|
|
7292
7332
|
};
|
|
7293
7333
|
}
|
|
7294
7334
|
function normalizeChartSpec(spec, warnings) {
|
|
@@ -9758,22 +9798,23 @@ var TEMPORAL_SCALE_TYPES = /* @__PURE__ */ new Set(["time", "utc"]);
|
|
|
9758
9798
|
function formatTickLabel(value2, resolvedScale) {
|
|
9759
9799
|
const axisConfig = resolvedScale.channel.axis || void 0;
|
|
9760
9800
|
const formatStr = axisConfig?.format;
|
|
9801
|
+
const suffix = axisConfig?.labelSuffix ?? "";
|
|
9761
9802
|
if (TEMPORAL_SCALE_TYPES.has(resolvedScale.type)) {
|
|
9762
9803
|
const temporalFmt = buildTemporalFormatter(formatStr);
|
|
9763
|
-
if (temporalFmt) return temporalFmt(value2);
|
|
9804
|
+
if (temporalFmt) return temporalFmt(value2) + suffix;
|
|
9764
9805
|
const useUtc = resolvedScale.type === "utc";
|
|
9765
|
-
return formatDate(value2, void 0, void 0, useUtc);
|
|
9806
|
+
return formatDate(value2, void 0, void 0, useUtc) + suffix;
|
|
9766
9807
|
}
|
|
9767
9808
|
if (NUMERIC_SCALE_TYPES.has(resolvedScale.type)) {
|
|
9768
9809
|
const num = value2;
|
|
9769
9810
|
if (formatStr) {
|
|
9770
9811
|
const fmt = buildD3Formatter5(formatStr);
|
|
9771
|
-
if (fmt) return fmt(num);
|
|
9812
|
+
if (fmt) return fmt(num) + suffix;
|
|
9772
9813
|
}
|
|
9773
|
-
if (Math.abs(num) >= 1e3) return abbreviateNumber2(num);
|
|
9774
|
-
return formatNumber3(num);
|
|
9814
|
+
if (Math.abs(num) >= 1e3) return abbreviateNumber2(num) + suffix;
|
|
9815
|
+
return formatNumber3(num) + suffix;
|
|
9775
9816
|
}
|
|
9776
|
-
return String(value2);
|
|
9817
|
+
return String(value2) + suffix;
|
|
9777
9818
|
}
|
|
9778
9819
|
function continuousTicks(resolvedScale, density, targetCount) {
|
|
9779
9820
|
const scale = resolvedScale.scale;
|
|
@@ -10426,7 +10467,8 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
|
|
|
10426
10467
|
const labelHeight = Math.min(rotatedHeight, 120);
|
|
10427
10468
|
xAxisHeight = hasXAxisLabel ? labelHeight + 20 : labelHeight;
|
|
10428
10469
|
} else {
|
|
10429
|
-
|
|
10470
|
+
const base = theme.spacing.xAxisHeight;
|
|
10471
|
+
xAxisHeight = hasXAxisLabel ? base + 22 : base;
|
|
10430
10472
|
}
|
|
10431
10473
|
const yAxisCfgPre = encoding.y?.axis ?? void 0;
|
|
10432
10474
|
const yTickPositionExplicitPre = yAxisCfgPre?.tickPosition;
|
|
@@ -10966,7 +11008,7 @@ function buildBandScale(channel, data, rangeStart, rangeEnd) {
|
|
|
10966
11008
|
}
|
|
10967
11009
|
function buildPointScale(channel, data, rangeStart, rangeEnd) {
|
|
10968
11010
|
const values = channel.scale?.domain ? channel.scale.domain : applyCategoricalSort(uniqueStrings(fieldValues(data, channel.field)), channel.sort);
|
|
10969
|
-
const padding = channel.scale?.padding ?? 0.5;
|
|
11011
|
+
const padding = channel.scale?.padding ?? channel.scale?.paddingOuter ?? 0.5;
|
|
10970
11012
|
const scale = point4().domain(values).range([rangeStart, rangeEnd]).padding(padding);
|
|
10971
11013
|
if (channel.scale?.reverse) {
|
|
10972
11014
|
const [r0, r1] = scale.range();
|