@opendata-ai/openchart-engine 7.1.3 → 7.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 +1 -1
- package/dist/index.js +55 -28
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/__snapshots__/compile-snapshot.test.ts.snap +51 -45
- package/src/__tests__/dimensions.test.ts +108 -0
- package/src/charts/bar/__tests__/labels.test.ts +72 -0
- package/src/charts/bar/index.ts +4 -1
- package/src/charts/bar/labels.ts +11 -6
- package/src/charts/column/index.ts +2 -0
- package/src/charts/column/labels.ts +9 -5
- 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 +15 -8
- package/src/layout/scales.ts +22 -4
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) {
|
|
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;
|
|
@@ -1815,16 +1817,16 @@ function computeBarLabels(marks, _chartArea, density = "auto", labelFormat, labe
|
|
|
1815
1817
|
let textAnchor;
|
|
1816
1818
|
if (isStacked2 && isInside) {
|
|
1817
1819
|
anchorX = mark.x + mark.width / 2;
|
|
1818
|
-
fill = pickLabelColor(bgColor);
|
|
1820
|
+
fill = pickLabelColor(bgColor, darkMode);
|
|
1819
1821
|
textAnchor = "middle";
|
|
1820
1822
|
} else if (isInside) {
|
|
1821
1823
|
if (isNegative) {
|
|
1822
1824
|
anchorX = mark.x + LABEL_PADDING;
|
|
1823
|
-
fill = pickLabelColor(bgColor);
|
|
1825
|
+
fill = pickLabelColor(bgColor, darkMode);
|
|
1824
1826
|
textAnchor = "start";
|
|
1825
1827
|
} else {
|
|
1826
1828
|
anchorX = mark.x + mark.width - LABEL_PADDING;
|
|
1827
|
-
fill = pickLabelColor(bgColor);
|
|
1829
|
+
fill = pickLabelColor(bgColor, darkMode);
|
|
1828
1830
|
textAnchor = "end";
|
|
1829
1831
|
}
|
|
1830
1832
|
} else {
|
|
@@ -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,
|
|
@@ -1898,7 +1900,7 @@ function computeBarLabels(marks, _chartArea, density = "auto", labelFormat, labe
|
|
|
1898
1900
|
}
|
|
1899
1901
|
|
|
1900
1902
|
// src/charts/bar/index.ts
|
|
1901
|
-
var barRenderer = (spec, scales, chartArea, strategy,
|
|
1903
|
+
var barRenderer = (spec, scales, chartArea, strategy, theme) => {
|
|
1902
1904
|
const marks = computeBarMarks(spec, scales, chartArea, strategy);
|
|
1903
1905
|
const valueField = spec.encoding?.x && "field" in spec.encoding.x ? spec.encoding.x.field : void 0;
|
|
1904
1906
|
const labels = computeBarLabels(
|
|
@@ -1908,7 +1910,10 @@ var barRenderer = (spec, scales, chartArea, strategy, _theme) => {
|
|
|
1908
1910
|
spec.labels.format,
|
|
1909
1911
|
spec.labels.prefix,
|
|
1910
1912
|
valueField,
|
|
1911
|
-
spec.labels.color
|
|
1913
|
+
spec.labels.color,
|
|
1914
|
+
theme.isDark,
|
|
1915
|
+
spec.labels.fontSize,
|
|
1916
|
+
spec.labels.suffix
|
|
1912
1917
|
);
|
|
1913
1918
|
for (let i = 0; i < marks.length && i < labels.length; i++) {
|
|
1914
1919
|
marks[i].label = labels[i];
|
|
@@ -2213,7 +2218,8 @@ import {
|
|
|
2213
2218
|
var LABEL_FONT_SIZE2 = 10;
|
|
2214
2219
|
var LABEL_FONT_WEIGHT2 = 600;
|
|
2215
2220
|
var LABEL_OFFSET_Y = 8;
|
|
2216
|
-
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;
|
|
2217
2223
|
const targetMarks = filterByDensity(marks, density);
|
|
2218
2224
|
const formatter = buildD3Formatter2(labelFormat);
|
|
2219
2225
|
const candidates = [];
|
|
@@ -2237,11 +2243,12 @@ function computeColumnLabels(marks, _chartArea, density = "auto", labelFormat, l
|
|
|
2237
2243
|
valuePart = rawValue;
|
|
2238
2244
|
}
|
|
2239
2245
|
}
|
|
2240
|
-
if (labelPrefix) valuePart = labelPrefix + valuePart;
|
|
2241
2246
|
const numericValue = parseFloat(valuePart);
|
|
2242
2247
|
const isNegative = Number.isFinite(numericValue) && numericValue < 0;
|
|
2243
|
-
|
|
2244
|
-
|
|
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;
|
|
2245
2252
|
const anchorX = mark.x + mark.width / 2;
|
|
2246
2253
|
const anchorY = isNegative ? mark.y + mark.height + LABEL_OFFSET_Y : mark.y - LABEL_OFFSET_Y - textHeight;
|
|
2247
2254
|
candidates.push({
|
|
@@ -2253,7 +2260,7 @@ function computeColumnLabels(marks, _chartArea, density = "auto", labelFormat, l
|
|
|
2253
2260
|
priority: "data",
|
|
2254
2261
|
style: {
|
|
2255
2262
|
fontFamily: "system-ui, -apple-system, sans-serif",
|
|
2256
|
-
fontSize:
|
|
2263
|
+
fontSize: FONT_SIZE,
|
|
2257
2264
|
fontWeight: LABEL_FONT_WEIGHT2,
|
|
2258
2265
|
fill: labelColor ?? getRepresentativeColor2(mark.fill),
|
|
2259
2266
|
lineHeight: 1.2,
|
|
@@ -2286,7 +2293,9 @@ var columnRenderer = (spec, scales, chartArea, strategy, _theme) => {
|
|
|
2286
2293
|
spec.labels.format,
|
|
2287
2294
|
spec.labels.prefix,
|
|
2288
2295
|
valueField,
|
|
2289
|
-
spec.labels.color
|
|
2296
|
+
spec.labels.color,
|
|
2297
|
+
spec.labels.fontSize,
|
|
2298
|
+
spec.labels.suffix
|
|
2290
2299
|
);
|
|
2291
2300
|
for (let i = 0; i < marks.length && i < labels.length; i++) {
|
|
2292
2301
|
marks[i].label = labels[i];
|
|
@@ -7286,8 +7295,10 @@ function normalizeLabels(labels) {
|
|
|
7286
7295
|
density: labels.density ?? "auto",
|
|
7287
7296
|
format: labels.format ?? "",
|
|
7288
7297
|
prefix: labels.prefix ?? "",
|
|
7298
|
+
suffix: labels.suffix,
|
|
7289
7299
|
offsets: labels.offsets,
|
|
7290
|
-
color: labels.color
|
|
7300
|
+
color: labels.color,
|
|
7301
|
+
fontSize: labels.fontSize
|
|
7291
7302
|
};
|
|
7292
7303
|
}
|
|
7293
7304
|
function normalizeChartSpec(spec, warnings) {
|
|
@@ -9757,22 +9768,23 @@ var TEMPORAL_SCALE_TYPES = /* @__PURE__ */ new Set(["time", "utc"]);
|
|
|
9757
9768
|
function formatTickLabel(value2, resolvedScale) {
|
|
9758
9769
|
const axisConfig = resolvedScale.channel.axis || void 0;
|
|
9759
9770
|
const formatStr = axisConfig?.format;
|
|
9771
|
+
const suffix = axisConfig?.labelSuffix ?? "";
|
|
9760
9772
|
if (TEMPORAL_SCALE_TYPES.has(resolvedScale.type)) {
|
|
9761
9773
|
const temporalFmt = buildTemporalFormatter(formatStr);
|
|
9762
|
-
if (temporalFmt) return temporalFmt(value2);
|
|
9774
|
+
if (temporalFmt) return temporalFmt(value2) + suffix;
|
|
9763
9775
|
const useUtc = resolvedScale.type === "utc";
|
|
9764
|
-
return formatDate(value2, void 0, void 0, useUtc);
|
|
9776
|
+
return formatDate(value2, void 0, void 0, useUtc) + suffix;
|
|
9765
9777
|
}
|
|
9766
9778
|
if (NUMERIC_SCALE_TYPES.has(resolvedScale.type)) {
|
|
9767
9779
|
const num = value2;
|
|
9768
9780
|
if (formatStr) {
|
|
9769
9781
|
const fmt = buildD3Formatter5(formatStr);
|
|
9770
|
-
if (fmt) return fmt(num);
|
|
9782
|
+
if (fmt) return fmt(num) + suffix;
|
|
9771
9783
|
}
|
|
9772
|
-
if (Math.abs(num) >= 1e3) return abbreviateNumber2(num);
|
|
9773
|
-
return formatNumber3(num);
|
|
9784
|
+
if (Math.abs(num) >= 1e3) return abbreviateNumber2(num) + suffix;
|
|
9785
|
+
return formatNumber3(num) + suffix;
|
|
9774
9786
|
}
|
|
9775
|
-
return String(value2);
|
|
9787
|
+
return String(value2) + suffix;
|
|
9776
9788
|
}
|
|
9777
9789
|
function continuousTicks(resolvedScale, density, targetCount) {
|
|
9778
9790
|
const scale = resolvedScale.scale;
|
|
@@ -10145,6 +10157,7 @@ function computeAxes(scales, chartArea, strategy, theme, measureText, dataContex
|
|
|
10145
10157
|
|
|
10146
10158
|
// src/layout/dimensions.ts
|
|
10147
10159
|
import {
|
|
10160
|
+
AXIS_TITLE_GAP,
|
|
10148
10161
|
AXIS_TITLE_TRAILING_PAD as AXIS_TITLE_TRAILING_PAD2,
|
|
10149
10162
|
BREAKPOINT_COMPACT_MAX as BREAKPOINT_COMPACT_MAX2,
|
|
10150
10163
|
computeChrome as computeChrome3,
|
|
@@ -10424,7 +10437,8 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
|
|
|
10424
10437
|
const labelHeight = Math.min(rotatedHeight, 120);
|
|
10425
10438
|
xAxisHeight = hasXAxisLabel ? labelHeight + 20 : labelHeight;
|
|
10426
10439
|
} else {
|
|
10427
|
-
|
|
10440
|
+
const base = theme.spacing.xAxisHeight;
|
|
10441
|
+
xAxisHeight = hasXAxisLabel ? base + 22 : base;
|
|
10428
10442
|
}
|
|
10429
10443
|
const yAxisCfgPre = encoding.y?.axis ?? void 0;
|
|
10430
10444
|
const yTickPositionExplicitPre = yAxisCfgPre?.tickPosition;
|
|
@@ -10609,7 +10623,6 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
|
|
|
10609
10623
|
theme.fonts.weights.normal
|
|
10610
10624
|
);
|
|
10611
10625
|
}
|
|
10612
|
-
const AXIS_TITLE_GAP = 8;
|
|
10613
10626
|
const dynamicTitleOffset = TICK_LABEL_OFFSET2 + estTickLabelWidth + AXIS_TITLE_GAP;
|
|
10614
10627
|
const axisTitleOffset = Math.max(dynamicTitleOffset, getAxisTitleOffset2(width));
|
|
10615
10628
|
const halfGlyph = Math.ceil(theme.fonts.sizes.body / 2);
|
|
@@ -10619,6 +10632,7 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
|
|
|
10619
10632
|
if (options.rightAxisReserve && options.rightAxisReserve > 0) {
|
|
10620
10633
|
margins.right = Math.max(margins.right, hPad + options.rightAxisReserve);
|
|
10621
10634
|
}
|
|
10635
|
+
const hasTopLegend = "entries" in legendLayout && legendLayout.entries.length > 0 && legendLayout.position === "top";
|
|
10622
10636
|
if ("entries" in legendLayout && legendLayout.entries.length > 0) {
|
|
10623
10637
|
const gap = legendGap(width);
|
|
10624
10638
|
if (legendLayout.position === "right" || legendLayout.position === "bottom-right") {
|
|
@@ -10627,7 +10641,7 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
|
|
|
10627
10641
|
margins.top += legendLayout.bounds.height + gap;
|
|
10628
10642
|
}
|
|
10629
10643
|
}
|
|
10630
|
-
margins.top += topAxisGap;
|
|
10644
|
+
margins.top += hasTopLegend ? inlineTickOverhang : topAxisGap;
|
|
10631
10645
|
let chartArea = {
|
|
10632
10646
|
x: margins.left,
|
|
10633
10647
|
y: margins.top,
|
|
@@ -10648,13 +10662,14 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
|
|
|
10648
10662
|
bottomLegendReservation
|
|
10649
10663
|
);
|
|
10650
10664
|
const fallbackTopAxisGap = isRadial && fallbackChrome.topHeight === 0 ? 0 : axisMargin + inlineTickOverhang;
|
|
10665
|
+
const fallbackEffectiveAxisGap = hasTopLegend ? inlineTickOverhang : fallbackTopAxisGap;
|
|
10651
10666
|
const newTop = topPad + fallbackChrome.topHeight + tentativeMetricsHeight;
|
|
10652
10667
|
const topDelta = margins.top - newTop;
|
|
10653
10668
|
const newBottom = bottomMargin(fallbackChrome.bottomHeight, padding, xAxisHeight);
|
|
10654
10669
|
const bottomDelta = margins.bottom - newBottom;
|
|
10655
10670
|
if (topDelta > 0 || bottomDelta > 0) {
|
|
10656
10671
|
const gap = legendGap(width);
|
|
10657
|
-
margins.top = newTop + ("entries" in legendLayout && legendLayout.entries.length > 0 && legendLayout.position === "top" ? legendLayout.bounds.height + gap : 0) +
|
|
10672
|
+
margins.top = newTop + ("entries" in legendLayout && legendLayout.entries.length > 0 && legendLayout.position === "top" ? legendLayout.bounds.height + gap : 0) + fallbackEffectiveAxisGap;
|
|
10658
10673
|
margins.bottom = newBottom;
|
|
10659
10674
|
chartArea = {
|
|
10660
10675
|
x: margins.left,
|
|
@@ -11059,7 +11074,13 @@ function computeScales(spec, chartArea, data) {
|
|
|
11059
11074
|
const xStackEnabled = encoding.x.stack === true || encoding.x.stack === "zero" || encoding.x.stack === "normalize" || encoding.x.stack === "center";
|
|
11060
11075
|
if (spec.markType === "bar" && encoding.color && encoding.x.type === "quantitative" && xStackEnabled) {
|
|
11061
11076
|
if (encoding.x.stack === "normalize") {
|
|
11062
|
-
|
|
11077
|
+
const existingAxis = encoding.x.axis;
|
|
11078
|
+
const axis = existingAxis === false || existingAxis?.format ? existingAxis : { ...typeof existingAxis === "object" ? existingAxis : {}, format: ".0%" };
|
|
11079
|
+
xChannel = {
|
|
11080
|
+
...encoding.x,
|
|
11081
|
+
scale: { ...encoding.x.scale, domain: [0, 1], nice: false },
|
|
11082
|
+
axis
|
|
11083
|
+
};
|
|
11063
11084
|
} else if (encoding.x.stack === "center") {
|
|
11064
11085
|
const yField = encoding.y?.field;
|
|
11065
11086
|
const xField = encoding.x.field;
|
|
@@ -11126,7 +11147,13 @@ function computeScales(spec, chartArea, data) {
|
|
|
11126
11147
|
}
|
|
11127
11148
|
if ((isBarStacked || isAreaStacked) && encoding.color && encoding.y.type === "quantitative") {
|
|
11128
11149
|
if (encoding.y.stack === "normalize") {
|
|
11129
|
-
|
|
11150
|
+
const existingAxis = encoding.y.axis;
|
|
11151
|
+
const axis = existingAxis === false || existingAxis?.format ? existingAxis : { ...typeof existingAxis === "object" ? existingAxis : {}, format: ".0%" };
|
|
11152
|
+
yChannel = {
|
|
11153
|
+
...encoding.y,
|
|
11154
|
+
scale: { ...encoding.y.scale, domain: [0, 1], nice: false },
|
|
11155
|
+
axis
|
|
11156
|
+
};
|
|
11130
11157
|
} else if (encoding.y.stack === "center") {
|
|
11131
11158
|
const xField = encoding.x?.field;
|
|
11132
11159
|
const yField = encoding.y.field;
|