@opendata-ai/openchart-engine 7.0.3 → 7.1.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.js +69 -26
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/__snapshots__/compile-snapshot.test.ts.snap +112 -108
- package/src/__tests__/compile-chart.test.ts +4 -1
- package/src/__tests__/dimensions.test.ts +6 -1
- package/src/charts/bar/__tests__/compute.test.ts +58 -56
- package/src/charts/bar/__tests__/labels.test.ts +3 -2
- package/src/charts/bar/compute.ts +8 -4
- package/src/charts/bar/labels.ts +5 -5
- package/src/charts/column/__tests__/compute.test.ts +22 -21
- package/src/charts/column/compute.ts +8 -3
- package/src/charts/column/labels.ts +7 -4
- package/src/charts/line/__tests__/compute.test.ts +20 -1
- package/src/charts/line/area.ts +12 -2
- package/src/charts/line/index.ts +2 -2
- package/src/endpoint-labels/__tests__/compute.test.ts +5 -1
- package/src/endpoint-labels/compute.ts +4 -2
- package/src/layout/axes/thinning.ts +4 -2
- package/src/layout/dimensions.ts +58 -4
- package/src/layout/scales.ts +8 -6
package/dist/index.js
CHANGED
|
@@ -1243,7 +1243,6 @@ function computeEndpointLabels(spec, marks, theme, chartArea, strategy) {
|
|
|
1243
1243
|
32
|
|
1244
1244
|
);
|
|
1245
1245
|
const columnX = chartArea.x + chartArea.width + ENDPOINT_COLUMN_GAP;
|
|
1246
|
-
const markerX = chartArea.x + chartArea.width;
|
|
1247
1246
|
const showLeader = config?.showLeader === true;
|
|
1248
1247
|
const entries = provisional.map((p, i) => {
|
|
1249
1248
|
const labelY = sweptTops[i];
|
|
@@ -1260,8 +1259,9 @@ function computeEndpointLabels(spec, marks, theme, chartArea, strategy) {
|
|
|
1260
1259
|
};
|
|
1261
1260
|
if (showMarker) {
|
|
1262
1261
|
entry.marker = {
|
|
1263
|
-
x:
|
|
1262
|
+
x: p.dataX + markerRadius,
|
|
1264
1263
|
y: p.dataY,
|
|
1264
|
+
dataX: p.dataX,
|
|
1265
1265
|
fill: markerFill,
|
|
1266
1266
|
stroke: config?.markerStyle?.stroke ?? p.color,
|
|
1267
1267
|
strokeWidth: markerStrokeWidth,
|
|
@@ -1493,8 +1493,8 @@ function computeBarMarks(spec, scales, _chartArea, _strategy) {
|
|
|
1493
1493
|
const categoryGroups = groupByField(spec.data, yChannel.field);
|
|
1494
1494
|
const needsStacking = Array.from(categoryGroups.values()).some((rows) => rows.length > 1);
|
|
1495
1495
|
if (needsStacking) {
|
|
1496
|
-
const
|
|
1497
|
-
if (
|
|
1496
|
+
const stackEnabled = xChannel.stack === true || xChannel.stack === "zero" || xChannel.stack === "normalize" || xChannel.stack === "center";
|
|
1497
|
+
if (!stackEnabled) {
|
|
1498
1498
|
marks = computeGroupedBars(
|
|
1499
1499
|
spec.data,
|
|
1500
1500
|
xChannel.field,
|
|
@@ -1738,8 +1738,8 @@ function computeSimpleBars(data, valueField, categoryField, xScale, yScale, band
|
|
|
1738
1738
|
import {
|
|
1739
1739
|
buildD3Formatter,
|
|
1740
1740
|
estimateTextWidth as estimateTextWidth4,
|
|
1741
|
-
findAccessibleColor,
|
|
1742
1741
|
getRepresentativeColor,
|
|
1742
|
+
pickLabelColor,
|
|
1743
1743
|
resolveCollisions
|
|
1744
1744
|
} from "@opendata-ai/openchart-core";
|
|
1745
1745
|
|
|
@@ -1806,7 +1806,7 @@ function computeBarLabels(marks, _chartArea, density = "auto", labelFormat, labe
|
|
|
1806
1806
|
if (labelPrefix) valuePart = labelPrefix + valuePart;
|
|
1807
1807
|
const textWidth = estimateTextWidth4(valuePart, LABEL_FONT_SIZE, LABEL_FONT_WEIGHT);
|
|
1808
1808
|
const textHeight = LABEL_FONT_SIZE * 1.2;
|
|
1809
|
-
const isStacked2 = mark.
|
|
1809
|
+
const isStacked2 = mark.stackGroup !== void 0;
|
|
1810
1810
|
const isInside = mark.width >= MIN_WIDTH_FOR_INSIDE_LABEL;
|
|
1811
1811
|
const isNegative = Number.isFinite(rawNum) ? rawNum < 0 : false;
|
|
1812
1812
|
const bgColor = getRepresentativeColor(mark.fill);
|
|
@@ -1815,16 +1815,16 @@ function computeBarLabels(marks, _chartArea, density = "auto", labelFormat, labe
|
|
|
1815
1815
|
let textAnchor;
|
|
1816
1816
|
if (isStacked2 && isInside) {
|
|
1817
1817
|
anchorX = mark.x + mark.width / 2;
|
|
1818
|
-
fill =
|
|
1818
|
+
fill = pickLabelColor(bgColor);
|
|
1819
1819
|
textAnchor = "middle";
|
|
1820
1820
|
} else if (isInside) {
|
|
1821
1821
|
if (isNegative) {
|
|
1822
1822
|
anchorX = mark.x + LABEL_PADDING;
|
|
1823
|
-
fill =
|
|
1823
|
+
fill = pickLabelColor(bgColor);
|
|
1824
1824
|
textAnchor = "start";
|
|
1825
1825
|
} else {
|
|
1826
1826
|
anchorX = mark.x + mark.width - LABEL_PADDING;
|
|
1827
|
-
fill =
|
|
1827
|
+
fill = pickLabelColor(bgColor);
|
|
1828
1828
|
textAnchor = "end";
|
|
1829
1829
|
}
|
|
1830
1830
|
} else {
|
|
@@ -1944,8 +1944,8 @@ function computeColumnMarks(spec, scales, _chartArea, _strategy) {
|
|
|
1944
1944
|
const categoryGroups = groupByField(spec.data, xChannel.field);
|
|
1945
1945
|
const needsStacking = Array.from(categoryGroups.values()).some((rows) => rows.length > 1);
|
|
1946
1946
|
if (needsStacking) {
|
|
1947
|
-
const
|
|
1948
|
-
if (
|
|
1947
|
+
const stackEnabled = yChannel.stack === true || yChannel.stack === "zero" || yChannel.stack === "normalize" || yChannel.stack === "center";
|
|
1948
|
+
if (!stackEnabled) {
|
|
1949
1949
|
marks = computeGroupedColumns(
|
|
1950
1950
|
spec.data,
|
|
1951
1951
|
xChannel.field,
|
|
@@ -2212,7 +2212,7 @@ import {
|
|
|
2212
2212
|
} from "@opendata-ai/openchart-core";
|
|
2213
2213
|
var LABEL_FONT_SIZE2 = 10;
|
|
2214
2214
|
var LABEL_FONT_WEIGHT2 = 600;
|
|
2215
|
-
var LABEL_OFFSET_Y =
|
|
2215
|
+
var LABEL_OFFSET_Y = 8;
|
|
2216
2216
|
function computeColumnLabels(marks, _chartArea, density = "auto", labelFormat, labelPrefix, valueField, labelColor) {
|
|
2217
2217
|
const targetMarks = filterByDensity(marks, density);
|
|
2218
2218
|
const formatter = buildD3Formatter2(labelFormat);
|
|
@@ -2258,7 +2258,7 @@ function computeColumnLabels(marks, _chartArea, density = "auto", labelFormat, l
|
|
|
2258
2258
|
fill: labelColor ?? getRepresentativeColor2(mark.fill),
|
|
2259
2259
|
lineHeight: 1.2,
|
|
2260
2260
|
textAnchor: "middle",
|
|
2261
|
-
dominantBaseline:
|
|
2261
|
+
dominantBaseline: "hanging"
|
|
2262
2262
|
}
|
|
2263
2263
|
});
|
|
2264
2264
|
}
|
|
@@ -3477,6 +3477,10 @@ var STACKED_GRADIENT_STOPS = [
|
|
|
3477
3477
|
{ offset: 0, opacity: 0.65 },
|
|
3478
3478
|
{ offset: 1, opacity: 0.35 }
|
|
3479
3479
|
];
|
|
3480
|
+
var STACKED_GRADIENT_STOPS_LIGHT = [
|
|
3481
|
+
{ offset: 0, opacity: 0.65 },
|
|
3482
|
+
{ offset: 1, opacity: 0 }
|
|
3483
|
+
];
|
|
3480
3484
|
function buildGradientFill(colorStr, stops) {
|
|
3481
3485
|
return {
|
|
3482
3486
|
gradient: "linear",
|
|
@@ -3569,7 +3573,7 @@ function computeSingleArea(spec, scales, _chartArea) {
|
|
|
3569
3573
|
}
|
|
3570
3574
|
return marks;
|
|
3571
3575
|
}
|
|
3572
|
-
function computeStackedArea(spec, scales, chartArea) {
|
|
3576
|
+
function computeStackedArea(spec, scales, chartArea, darkMode) {
|
|
3573
3577
|
const encoding = spec.encoding;
|
|
3574
3578
|
const xChannel = encoding.x;
|
|
3575
3579
|
const yChannel = encoding.y;
|
|
@@ -3647,7 +3651,8 @@ function computeStackedArea(spec, scales, chartArea) {
|
|
|
3647
3651
|
fillOpacity = isGradientDef3(markFill) ? 1 : spec.markDef.opacity ?? 0.7;
|
|
3648
3652
|
} else {
|
|
3649
3653
|
const colorStr = getRepresentativeColor4(color2);
|
|
3650
|
-
|
|
3654
|
+
const stackedStops = darkMode ? STACKED_GRADIENT_STOPS : STACKED_GRADIENT_STOPS_LIGHT;
|
|
3655
|
+
fillValue = buildGradientFill(colorStr, stackedStops);
|
|
3651
3656
|
fillOpacity = 1;
|
|
3652
3657
|
}
|
|
3653
3658
|
marks.push({
|
|
@@ -3675,11 +3680,11 @@ function computeStackedArea(spec, scales, chartArea) {
|
|
|
3675
3680
|
}
|
|
3676
3681
|
return marks;
|
|
3677
3682
|
}
|
|
3678
|
-
function computeAreaMarks(spec, scales, chartArea) {
|
|
3683
|
+
function computeAreaMarks(spec, scales, chartArea, darkMode) {
|
|
3679
3684
|
const encoding = spec.encoding;
|
|
3680
3685
|
const yChannel = encoding.y;
|
|
3681
3686
|
if (yChannel && isStacked(yChannel.stack)) {
|
|
3682
|
-
return computeStackedArea(spec, scales, chartArea);
|
|
3687
|
+
return computeStackedArea(spec, scales, chartArea, darkMode);
|
|
3683
3688
|
}
|
|
3684
3689
|
return computeSingleArea(spec, scales, chartArea);
|
|
3685
3690
|
}
|
|
@@ -3938,8 +3943,8 @@ var lineRenderer = (spec, scales, chartArea, strategy, _theme) => {
|
|
|
3938
3943
|
}
|
|
3939
3944
|
return marks;
|
|
3940
3945
|
};
|
|
3941
|
-
var areaRenderer = (spec, scales, chartArea, strategy,
|
|
3942
|
-
const areas = computeAreaMarks(spec, scales, chartArea);
|
|
3946
|
+
var areaRenderer = (spec, scales, chartArea, strategy, theme) => {
|
|
3947
|
+
const areas = computeAreaMarks(spec, scales, chartArea, theme.isDark);
|
|
3943
3948
|
const encoding = spec.encoding;
|
|
3944
3949
|
const hasColor = !!(encoding.color && "field" in encoding.color);
|
|
3945
3950
|
const lines = hasColor ? linesFromAreas(areas) : computeLineMarks(spec, scales, chartArea, strategy);
|
|
@@ -9657,7 +9662,7 @@ var DEFAULT_COLLISION_PADDING = 5;
|
|
|
9657
9662
|
|
|
9658
9663
|
// src/layout/axes/thinning.ts
|
|
9659
9664
|
import { estimateTextWidth as estimateTextWidth11 } from "@opendata-ai/openchart-core";
|
|
9660
|
-
var MIN_TICK_GAP_FACTOR =
|
|
9665
|
+
var MIN_TICK_GAP_FACTOR = 0.5;
|
|
9661
9666
|
var MIN_TICK_COUNT = 2;
|
|
9662
9667
|
function measureLabel(text, fontSize, fontWeight, measureText) {
|
|
9663
9668
|
return measureText ? measureText(text, fontSize, fontWeight).width : estimateTextWidth11(text, fontSize, fontWeight);
|
|
@@ -10159,6 +10164,7 @@ import {
|
|
|
10159
10164
|
MAX_LEFT_LABEL_FRACTION_MEDIUM,
|
|
10160
10165
|
MAX_LEFT_LABEL_FRACTION_MEDIUM_MAX,
|
|
10161
10166
|
NARROW_VIEWPORT_MAX,
|
|
10167
|
+
TICK_LABEL_OFFSET as TICK_LABEL_OFFSET2,
|
|
10162
10168
|
TOP_PAD_EXTRA_NARROW
|
|
10163
10169
|
} from "@opendata-ai/openchart-core";
|
|
10164
10170
|
|
|
@@ -10316,6 +10322,9 @@ function chromeToInput(chrome) {
|
|
|
10316
10322
|
brand: chrome.brand
|
|
10317
10323
|
};
|
|
10318
10324
|
}
|
|
10325
|
+
function bottomMargin(bottomHeight, padding, xAxisHeight) {
|
|
10326
|
+
return (bottomHeight > 0 ? bottomHeight : padding) + xAxisHeight;
|
|
10327
|
+
}
|
|
10319
10328
|
function scalePadding(basePadding, width, height) {
|
|
10320
10329
|
const minDim = Math.min(width, height);
|
|
10321
10330
|
if (minDim >= 500) return basePadding;
|
|
@@ -10436,7 +10445,7 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
|
|
|
10436
10445
|
const margins = {
|
|
10437
10446
|
top: topPad + chrome.topHeight + tentativeMetricsHeight,
|
|
10438
10447
|
right: hPad + (isRadial ? hPad : axisMargin),
|
|
10439
|
-
bottom:
|
|
10448
|
+
bottom: bottomMargin(chrome.bottomHeight, padding, xAxisHeight),
|
|
10440
10449
|
left: hPad + (isRadial ? hPad : axisMargin)
|
|
10441
10450
|
};
|
|
10442
10451
|
const labelDensity = spec.labels.density;
|
|
@@ -10573,7 +10582,41 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
|
|
|
10573
10582
|
}
|
|
10574
10583
|
const yAxis = encoding.y?.axis;
|
|
10575
10584
|
if (yAxis && (yAxis.title || yAxis.label) && !isRadial) {
|
|
10576
|
-
const
|
|
10585
|
+
const yFieldForTitle = encoding.y?.field;
|
|
10586
|
+
const yAxisFormatForTitle = yAxis?.format;
|
|
10587
|
+
let estTickLabelWidth = 0;
|
|
10588
|
+
if (yFieldForTitle && (encoding.y?.type === "quantitative" || encoding.y?.type === "temporal")) {
|
|
10589
|
+
let maxAbsValForTitle = 0;
|
|
10590
|
+
for (const row of spec.data) {
|
|
10591
|
+
const v = Number(row[yFieldForTitle]);
|
|
10592
|
+
if (Number.isFinite(v) && Math.abs(v) > maxAbsValForTitle) maxAbsValForTitle = Math.abs(v);
|
|
10593
|
+
}
|
|
10594
|
+
let sampleLabelForTitle;
|
|
10595
|
+
if (yAxisFormatForTitle) {
|
|
10596
|
+
try {
|
|
10597
|
+
const fmt = format(yAxisFormatForTitle);
|
|
10598
|
+
sampleLabelForTitle = fmt(maxAbsValForTitle);
|
|
10599
|
+
} catch {
|
|
10600
|
+
sampleLabelForTitle = String(maxAbsValForTitle);
|
|
10601
|
+
}
|
|
10602
|
+
} else {
|
|
10603
|
+
if (maxAbsValForTitle >= 1e9) sampleLabelForTitle = "1.5B";
|
|
10604
|
+
else if (maxAbsValForTitle >= 1e6) sampleLabelForTitle = "1.5M";
|
|
10605
|
+
else if (maxAbsValForTitle >= 1e3) sampleLabelForTitle = "1.5K";
|
|
10606
|
+
else if (maxAbsValForTitle >= 100) sampleLabelForTitle = "100";
|
|
10607
|
+
else if (maxAbsValForTitle >= 10) sampleLabelForTitle = "10";
|
|
10608
|
+
else sampleLabelForTitle = "0.0";
|
|
10609
|
+
}
|
|
10610
|
+
const negPrefixForTitle = spec.data.some((r) => Number(r[yFieldForTitle]) < 0) ? "-" : "";
|
|
10611
|
+
estTickLabelWidth = estimateTextWidth16(
|
|
10612
|
+
negPrefixForTitle + sampleLabelForTitle,
|
|
10613
|
+
theme.fonts.sizes.axisTick,
|
|
10614
|
+
theme.fonts.weights.normal
|
|
10615
|
+
);
|
|
10616
|
+
}
|
|
10617
|
+
const AXIS_TITLE_GAP = 8;
|
|
10618
|
+
const dynamicTitleOffset = TICK_LABEL_OFFSET2 + estTickLabelWidth + AXIS_TITLE_GAP;
|
|
10619
|
+
const axisTitleOffset = Math.max(dynamicTitleOffset, getAxisTitleOffset2(width));
|
|
10577
10620
|
const halfGlyph = Math.ceil(theme.fonts.sizes.body / 2);
|
|
10578
10621
|
const rotatedLabelMargin = axisTitleOffset + halfGlyph + (width < BREAKPOINT_COMPACT_MAX2 ? 0 : AXIS_TITLE_TRAILING_PAD2);
|
|
10579
10622
|
margins.left = Math.max(margins.left, hPad + rotatedLabelMargin);
|
|
@@ -10612,7 +10655,7 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
|
|
|
10612
10655
|
const fallbackTopAxisGap = isRadial && fallbackChrome.topHeight === 0 ? 0 : axisMargin + inlineTickOverhang;
|
|
10613
10656
|
const newTop = topPad + fallbackChrome.topHeight + tentativeMetricsHeight;
|
|
10614
10657
|
const topDelta = margins.top - newTop;
|
|
10615
|
-
const newBottom =
|
|
10658
|
+
const newBottom = bottomMargin(fallbackChrome.bottomHeight, padding, xAxisHeight);
|
|
10616
10659
|
const bottomDelta = margins.bottom - newBottom;
|
|
10617
10660
|
if (topDelta > 0 || bottomDelta > 0) {
|
|
10618
10661
|
const gap = legendGap(width);
|
|
@@ -11018,8 +11061,8 @@ function computeScales(spec, chartArea, data) {
|
|
|
11018
11061
|
if (encoding.x) {
|
|
11019
11062
|
let xData = data;
|
|
11020
11063
|
let xChannel = encoding.x;
|
|
11021
|
-
const
|
|
11022
|
-
if (spec.markType === "bar" && encoding.color && encoding.x.type === "quantitative" &&
|
|
11064
|
+
const xStackEnabled = encoding.x.stack === true || encoding.x.stack === "zero" || encoding.x.stack === "normalize" || encoding.x.stack === "center";
|
|
11065
|
+
if (spec.markType === "bar" && encoding.color && encoding.x.type === "quantitative" && xStackEnabled) {
|
|
11023
11066
|
if (encoding.x.stack === "normalize") {
|
|
11024
11067
|
xChannel = { ...encoding.x, scale: { ...encoding.x.scale, domain: [0, 1], nice: false } };
|
|
11025
11068
|
} else if (encoding.x.stack === "center") {
|
|
@@ -11074,7 +11117,7 @@ function computeScales(spec, chartArea, data) {
|
|
|
11074
11117
|
const stackProp = encoding.y.stack;
|
|
11075
11118
|
const isExplicitlyStacked = stackProp === true || stackProp === "zero" || stackProp === "normalize" || stackProp === "center";
|
|
11076
11119
|
const isAreaStacked = spec.markType === "area" && isExplicitlyStacked;
|
|
11077
|
-
const isBarStacked = isVerticalBar &&
|
|
11120
|
+
const isBarStacked = isVerticalBar && isExplicitlyStacked;
|
|
11078
11121
|
const hasStackingGroup = isBarStacked && encoding.color !== void 0;
|
|
11079
11122
|
const userRequestedStack = isExplicitlyStacked;
|
|
11080
11123
|
const isLineOrArea2 = spec.markType === "line" || spec.markType === "area";
|