@opendata-ai/openchart-engine 2.9.0 → 2.10.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 +145 -21
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/__test-fixtures__/specs.ts +4 -0
- package/src/__tests__/axes.test.ts +4 -0
- package/src/__tests__/legend.test.ts +4 -0
- package/src/compile.ts +5 -3
- package/src/layout/axes.ts +22 -1
- package/src/layout/dimensions.ts +77 -4
- package/src/legend/compute.ts +102 -5
package/dist/index.js
CHANGED
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
generateAltText,
|
|
5
5
|
generateDataTable,
|
|
6
6
|
getBreakpoint,
|
|
7
|
+
getHeightClass,
|
|
7
8
|
getLayoutStrategy,
|
|
8
9
|
resolveTheme as resolveTheme2
|
|
9
10
|
} from "@opendata-ai/openchart-core";
|
|
@@ -5979,6 +5980,7 @@ var DEFAULT_COLLISION_PADDING = 5;
|
|
|
5979
5980
|
import {
|
|
5980
5981
|
abbreviateNumber as abbreviateNumber3,
|
|
5981
5982
|
buildD3Formatter as buildD3Formatter3,
|
|
5983
|
+
estimateTextWidth as estimateTextWidth7,
|
|
5982
5984
|
formatDate,
|
|
5983
5985
|
formatNumber as formatNumber3
|
|
5984
5986
|
} from "@opendata-ai/openchart-core";
|
|
@@ -6086,13 +6088,29 @@ function computeAxes(scales, chartArea, strategy, theme) {
|
|
|
6086
6088
|
position: t.position,
|
|
6087
6089
|
major: true
|
|
6088
6090
|
}));
|
|
6091
|
+
let tickAngle = scales.x.channel.axis?.tickAngle;
|
|
6092
|
+
if (tickAngle === void 0 && scales.x.type === "band" && ticks2.length > 1) {
|
|
6093
|
+
const bandwidth = scales.x.scale.bandwidth();
|
|
6094
|
+
let maxLabelWidth = 0;
|
|
6095
|
+
for (const t of ticks2) {
|
|
6096
|
+
const w = estimateTextWidth7(
|
|
6097
|
+
t.label,
|
|
6098
|
+
theme.fonts.sizes.axisTick,
|
|
6099
|
+
theme.fonts.weights.normal
|
|
6100
|
+
);
|
|
6101
|
+
if (w > maxLabelWidth) maxLabelWidth = w;
|
|
6102
|
+
}
|
|
6103
|
+
if (maxLabelWidth > bandwidth * 0.85) {
|
|
6104
|
+
tickAngle = -45;
|
|
6105
|
+
}
|
|
6106
|
+
}
|
|
6089
6107
|
result.x = {
|
|
6090
6108
|
ticks: ticks2,
|
|
6091
6109
|
gridlines: scales.x.channel.axis?.grid ? gridlines : [],
|
|
6092
6110
|
label: scales.x.channel.axis?.label,
|
|
6093
6111
|
labelStyle: axisLabelStyle,
|
|
6094
6112
|
tickLabelStyle,
|
|
6095
|
-
tickAngle
|
|
6113
|
+
tickAngle,
|
|
6096
6114
|
start: { x: chartArea.x, y: chartArea.y + chartArea.height },
|
|
6097
6115
|
end: { x: chartArea.x + chartArea.width, y: chartArea.y + chartArea.height }
|
|
6098
6116
|
};
|
|
@@ -6119,7 +6137,7 @@ function computeAxes(scales, chartArea, strategy, theme) {
|
|
|
6119
6137
|
}
|
|
6120
6138
|
|
|
6121
6139
|
// src/layout/dimensions.ts
|
|
6122
|
-
import { computeChrome as computeChrome2, estimateTextWidth as
|
|
6140
|
+
import { computeChrome as computeChrome2, estimateTextWidth as estimateTextWidth8 } from "@opendata-ai/openchart-core";
|
|
6123
6141
|
function chromeToInput(chrome) {
|
|
6124
6142
|
return {
|
|
6125
6143
|
title: chrome.title,
|
|
@@ -6129,11 +6147,28 @@ function chromeToInput(chrome) {
|
|
|
6129
6147
|
footer: chrome.footer
|
|
6130
6148
|
};
|
|
6131
6149
|
}
|
|
6132
|
-
function
|
|
6150
|
+
function scalePadding(basePadding, width, height) {
|
|
6151
|
+
const minDim = Math.min(width, height);
|
|
6152
|
+
if (minDim >= 500) return basePadding;
|
|
6153
|
+
if (minDim <= 200) return Math.max(Math.round(basePadding * 0.5), 4);
|
|
6154
|
+
const t = (minDim - 200) / 300;
|
|
6155
|
+
return Math.max(Math.round(basePadding * (0.5 + t * 0.5)), 4);
|
|
6156
|
+
}
|
|
6157
|
+
var MIN_CHART_WIDTH = 60;
|
|
6158
|
+
var MIN_CHART_HEIGHT = 40;
|
|
6159
|
+
function computeDimensions(spec, options, legendLayout, theme, strategy) {
|
|
6133
6160
|
const { width, height } = options;
|
|
6134
|
-
const padding = theme.spacing.padding;
|
|
6161
|
+
const padding = scalePadding(theme.spacing.padding, width, height);
|
|
6135
6162
|
const axisMargin = theme.spacing.axisMargin;
|
|
6136
|
-
const
|
|
6163
|
+
const chromeMode = strategy?.chromeMode ?? "full";
|
|
6164
|
+
const chrome = computeChrome2(
|
|
6165
|
+
chromeToInput(spec.chrome),
|
|
6166
|
+
theme,
|
|
6167
|
+
width,
|
|
6168
|
+
options.measureText,
|
|
6169
|
+
chromeMode,
|
|
6170
|
+
padding
|
|
6171
|
+
);
|
|
6137
6172
|
const total = { x: 0, y: 0, width, height };
|
|
6138
6173
|
const isRadial = spec.type === "pie" || spec.type === "donut";
|
|
6139
6174
|
const encoding = spec.encoding;
|
|
@@ -6150,7 +6185,7 @@ function computeDimensions(spec, options, legendLayout, theme) {
|
|
|
6150
6185
|
if (xField) {
|
|
6151
6186
|
for (const row of spec.data) {
|
|
6152
6187
|
const label = String(row[xField] ?? "");
|
|
6153
|
-
const w =
|
|
6188
|
+
const w = estimateTextWidth8(label, theme.fonts.sizes.axisTick, theme.fonts.weights.normal);
|
|
6154
6189
|
if (w > maxLabelWidth) maxLabelWidth = w;
|
|
6155
6190
|
}
|
|
6156
6191
|
}
|
|
@@ -6176,7 +6211,7 @@ function computeDimensions(spec, options, legendLayout, theme) {
|
|
|
6176
6211
|
const label = String(row[colorField] ?? "");
|
|
6177
6212
|
if (!seen.has(label)) {
|
|
6178
6213
|
seen.add(label);
|
|
6179
|
-
const w =
|
|
6214
|
+
const w = estimateTextWidth8(label, 11, 600);
|
|
6180
6215
|
if (w > maxLabelWidth) maxLabelWidth = w;
|
|
6181
6216
|
}
|
|
6182
6217
|
}
|
|
@@ -6191,7 +6226,7 @@ function computeDimensions(spec, options, legendLayout, theme) {
|
|
|
6191
6226
|
let maxLabelWidth = 0;
|
|
6192
6227
|
for (const row of spec.data) {
|
|
6193
6228
|
const label = String(row[yField] ?? "");
|
|
6194
|
-
const w =
|
|
6229
|
+
const w = estimateTextWidth8(label, theme.fonts.sizes.axisTick, theme.fonts.weights.normal);
|
|
6195
6230
|
if (w > maxLabelWidth) maxLabelWidth = w;
|
|
6196
6231
|
}
|
|
6197
6232
|
if (maxLabelWidth > 0) {
|
|
@@ -6213,7 +6248,7 @@ function computeDimensions(spec, options, legendLayout, theme) {
|
|
|
6213
6248
|
else sampleLabel = "0.0";
|
|
6214
6249
|
const negPrefix = spec.data.some((r) => Number(r[yField]) < 0) ? "-" : "";
|
|
6215
6250
|
const labelEst = negPrefix + sampleLabel;
|
|
6216
|
-
const labelWidth =
|
|
6251
|
+
const labelWidth = estimateTextWidth8(
|
|
6217
6252
|
labelEst,
|
|
6218
6253
|
theme.fonts.sizes.axisTick,
|
|
6219
6254
|
theme.fonts.weights.normal
|
|
@@ -6234,12 +6269,38 @@ function computeDimensions(spec, options, legendLayout, theme) {
|
|
|
6234
6269
|
margins.bottom += legendLayout.bounds.height + 4;
|
|
6235
6270
|
}
|
|
6236
6271
|
}
|
|
6237
|
-
|
|
6272
|
+
let chartArea = {
|
|
6238
6273
|
x: margins.left,
|
|
6239
6274
|
y: margins.top,
|
|
6240
6275
|
width: Math.max(0, width - margins.left - margins.right),
|
|
6241
6276
|
height: Math.max(0, height - margins.top - margins.bottom)
|
|
6242
6277
|
};
|
|
6278
|
+
if ((chartArea.width < MIN_CHART_WIDTH || chartArea.height < MIN_CHART_HEIGHT) && chromeMode !== "hidden") {
|
|
6279
|
+
const fallbackMode = chromeMode === "full" ? "compact" : "hidden";
|
|
6280
|
+
const fallbackChrome = computeChrome2(
|
|
6281
|
+
chromeToInput(spec.chrome),
|
|
6282
|
+
theme,
|
|
6283
|
+
width,
|
|
6284
|
+
options.measureText,
|
|
6285
|
+
fallbackMode,
|
|
6286
|
+
padding
|
|
6287
|
+
);
|
|
6288
|
+
const newTop = padding + fallbackChrome.topHeight + axisMargin;
|
|
6289
|
+
const topDelta = margins.top - newTop;
|
|
6290
|
+
const newBottom = padding + fallbackChrome.bottomHeight + xAxisHeight;
|
|
6291
|
+
const bottomDelta = margins.bottom - newBottom;
|
|
6292
|
+
if (topDelta > 0 || bottomDelta > 0) {
|
|
6293
|
+
margins.top = newTop + (legendLayout.entries.length > 0 && legendLayout.position === "top" ? legendLayout.bounds.height + 4 : 0);
|
|
6294
|
+
margins.bottom = newBottom;
|
|
6295
|
+
chartArea = {
|
|
6296
|
+
x: margins.left,
|
|
6297
|
+
y: margins.top,
|
|
6298
|
+
width: Math.max(0, width - margins.left - margins.right),
|
|
6299
|
+
height: Math.max(0, height - margins.top - margins.bottom)
|
|
6300
|
+
};
|
|
6301
|
+
return { total, chrome: fallbackChrome, chartArea, margins, theme };
|
|
6302
|
+
}
|
|
6303
|
+
}
|
|
6243
6304
|
return { total, chrome, chartArea, margins, theme };
|
|
6244
6305
|
}
|
|
6245
6306
|
|
|
@@ -6482,12 +6543,14 @@ function computeScales(spec, chartArea, data) {
|
|
|
6482
6543
|
}
|
|
6483
6544
|
|
|
6484
6545
|
// src/legend/compute.ts
|
|
6485
|
-
import { estimateTextWidth as
|
|
6546
|
+
import { estimateTextWidth as estimateTextWidth9 } from "@opendata-ai/openchart-core";
|
|
6486
6547
|
var SWATCH_SIZE2 = 12;
|
|
6487
6548
|
var SWATCH_GAP2 = 6;
|
|
6488
6549
|
var ENTRY_GAP2 = 16;
|
|
6489
6550
|
var LEGEND_PADDING = 8;
|
|
6490
6551
|
var LEGEND_RIGHT_WIDTH = 120;
|
|
6552
|
+
var RIGHT_LEGEND_MAX_HEIGHT_RATIO = 0.4;
|
|
6553
|
+
var TOP_LEGEND_MAX_ROWS = 2;
|
|
6491
6554
|
function swatchShapeForType(chartType) {
|
|
6492
6555
|
switch (chartType) {
|
|
6493
6556
|
case "line":
|
|
@@ -6513,8 +6576,41 @@ function extractColorEntries(spec, theme) {
|
|
|
6513
6576
|
active: true
|
|
6514
6577
|
}));
|
|
6515
6578
|
}
|
|
6579
|
+
function entriesThatFit(entries, maxWidth, maxRows, labelStyle) {
|
|
6580
|
+
let row = 1;
|
|
6581
|
+
let rowWidth = 0;
|
|
6582
|
+
for (let i = 0; i < entries.length; i++) {
|
|
6583
|
+
const labelWidth = estimateTextWidth9(
|
|
6584
|
+
entries[i].label,
|
|
6585
|
+
labelStyle.fontSize,
|
|
6586
|
+
labelStyle.fontWeight
|
|
6587
|
+
);
|
|
6588
|
+
const entryWidth = SWATCH_SIZE2 + SWATCH_GAP2 + labelWidth + ENTRY_GAP2;
|
|
6589
|
+
if (rowWidth + entryWidth > maxWidth && rowWidth > 0) {
|
|
6590
|
+
row++;
|
|
6591
|
+
rowWidth = entryWidth;
|
|
6592
|
+
if (row > maxRows) return i;
|
|
6593
|
+
} else {
|
|
6594
|
+
rowWidth += entryWidth;
|
|
6595
|
+
}
|
|
6596
|
+
}
|
|
6597
|
+
return entries.length;
|
|
6598
|
+
}
|
|
6599
|
+
function truncateEntries(entries, maxCount) {
|
|
6600
|
+
if (maxCount >= entries.length || maxCount <= 0) return entries;
|
|
6601
|
+
const truncated = entries.slice(0, maxCount);
|
|
6602
|
+
const remaining = entries.length - maxCount;
|
|
6603
|
+
truncated.push({
|
|
6604
|
+
label: `+${remaining} more`,
|
|
6605
|
+
color: "#999999",
|
|
6606
|
+
shape: "square",
|
|
6607
|
+
active: false,
|
|
6608
|
+
overflow: true
|
|
6609
|
+
});
|
|
6610
|
+
return truncated;
|
|
6611
|
+
}
|
|
6516
6612
|
function computeLegend(spec, strategy, theme, chartArea) {
|
|
6517
|
-
if (spec.legend?.show === false) {
|
|
6613
|
+
if (spec.legend?.show === false || strategy.legendMaxHeight === 0) {
|
|
6518
6614
|
return {
|
|
6519
6615
|
position: "top",
|
|
6520
6616
|
entries: [],
|
|
@@ -6531,7 +6627,7 @@ function computeLegend(spec, strategy, theme, chartArea) {
|
|
|
6531
6627
|
entryGap: ENTRY_GAP2
|
|
6532
6628
|
};
|
|
6533
6629
|
}
|
|
6534
|
-
|
|
6630
|
+
let entries = extractColorEntries(spec, theme);
|
|
6535
6631
|
const labelStyle = {
|
|
6536
6632
|
fontFamily: theme.fonts.family,
|
|
6537
6633
|
fontSize: theme.fonts.sizes.small,
|
|
@@ -6553,13 +6649,22 @@ function computeLegend(spec, strategy, theme, chartArea) {
|
|
|
6553
6649
|
}
|
|
6554
6650
|
if (resolvedPosition === "right" || resolvedPosition === "bottom-right") {
|
|
6555
6651
|
const maxLabelWidth = Math.max(
|
|
6556
|
-
...entries.map((e) =>
|
|
6652
|
+
...entries.map((e) => estimateTextWidth9(e.label, labelStyle.fontSize, labelStyle.fontWeight))
|
|
6557
6653
|
);
|
|
6558
6654
|
const legendWidth = Math.min(
|
|
6559
6655
|
LEGEND_RIGHT_WIDTH,
|
|
6560
6656
|
SWATCH_SIZE2 + SWATCH_GAP2 + maxLabelWidth + LEGEND_PADDING * 2
|
|
6561
6657
|
);
|
|
6562
6658
|
const entryHeight = Math.max(SWATCH_SIZE2, labelStyle.fontSize * labelStyle.lineHeight);
|
|
6659
|
+
const maxHeightRatio = strategy.legendMaxHeight > 0 ? strategy.legendMaxHeight : RIGHT_LEGEND_MAX_HEIGHT_RATIO;
|
|
6660
|
+
const maxLegendHeight = chartArea.height * maxHeightRatio;
|
|
6661
|
+
const maxEntries = Math.max(
|
|
6662
|
+
1,
|
|
6663
|
+
Math.floor((maxLegendHeight - LEGEND_PADDING * 2) / (entryHeight + 4))
|
|
6664
|
+
);
|
|
6665
|
+
if (entries.length > maxEntries) {
|
|
6666
|
+
entries = truncateEntries(entries, maxEntries);
|
|
6667
|
+
}
|
|
6563
6668
|
const legendHeight2 = entries.length * entryHeight + (entries.length - 1) * 4 + LEGEND_PADDING * 2;
|
|
6564
6669
|
const clampedHeight = Math.min(legendHeight2, chartArea.height);
|
|
6565
6670
|
const legendY = resolvedPosition === "bottom-right" ? chartArea.y + chartArea.height - clampedHeight : chartArea.y;
|
|
@@ -6580,11 +6685,29 @@ function computeLegend(spec, strategy, theme, chartArea) {
|
|
|
6580
6685
|
entryGap: 4
|
|
6581
6686
|
};
|
|
6582
6687
|
}
|
|
6688
|
+
const availableWidth = chartArea.width - LEGEND_PADDING * 2;
|
|
6689
|
+
const maxFit = entriesThatFit(entries, availableWidth, TOP_LEGEND_MAX_ROWS, labelStyle);
|
|
6690
|
+
if (maxFit < entries.length) {
|
|
6691
|
+
entries = truncateEntries(entries, maxFit);
|
|
6692
|
+
}
|
|
6583
6693
|
const totalWidth = entries.reduce((sum, entry) => {
|
|
6584
|
-
const labelWidth =
|
|
6694
|
+
const labelWidth = estimateTextWidth9(entry.label, labelStyle.fontSize, labelStyle.fontWeight);
|
|
6585
6695
|
return sum + SWATCH_SIZE2 + SWATCH_GAP2 + labelWidth + ENTRY_GAP2;
|
|
6586
6696
|
}, 0);
|
|
6587
|
-
|
|
6697
|
+
let rowCount = 1;
|
|
6698
|
+
let rowWidth = 0;
|
|
6699
|
+
for (const entry of entries) {
|
|
6700
|
+
const labelWidth = estimateTextWidth9(entry.label, labelStyle.fontSize, labelStyle.fontWeight);
|
|
6701
|
+
const entryWidth = SWATCH_SIZE2 + SWATCH_GAP2 + labelWidth + ENTRY_GAP2;
|
|
6702
|
+
if (rowWidth + entryWidth > availableWidth && rowWidth > 0) {
|
|
6703
|
+
rowCount++;
|
|
6704
|
+
rowWidth = entryWidth;
|
|
6705
|
+
} else {
|
|
6706
|
+
rowWidth += entryWidth;
|
|
6707
|
+
}
|
|
6708
|
+
}
|
|
6709
|
+
const rowHeight = SWATCH_SIZE2 + 4;
|
|
6710
|
+
const legendHeight = rowCount * rowHeight + LEGEND_PADDING * 2;
|
|
6588
6711
|
const offsetDx = spec.legend?.offset?.dx ?? 0;
|
|
6589
6712
|
const offsetDy = spec.legend?.offset?.dy ?? 0;
|
|
6590
6713
|
return {
|
|
@@ -6604,7 +6727,7 @@ function computeLegend(spec, strategy, theme, chartArea) {
|
|
|
6604
6727
|
}
|
|
6605
6728
|
|
|
6606
6729
|
// src/tables/compile-table.ts
|
|
6607
|
-
import { computeChrome as computeChrome3, estimateTextWidth as
|
|
6730
|
+
import { computeChrome as computeChrome3, estimateTextWidth as estimateTextWidth10 } from "@opendata-ai/openchart-core";
|
|
6608
6731
|
|
|
6609
6732
|
// src/tables/bar-column.ts
|
|
6610
6733
|
var NEGATIVE_BAR_COLOR = "#c44e52";
|
|
@@ -7011,13 +7134,13 @@ function estimateColumnWidth(col, data, fontSize) {
|
|
|
7011
7134
|
if (col.image) return (col.image.width ?? 24) + PADDING;
|
|
7012
7135
|
if (col.flag) return 60;
|
|
7013
7136
|
const label = col.label ?? col.key;
|
|
7014
|
-
const headerWidth =
|
|
7137
|
+
const headerWidth = estimateTextWidth10(label, fontSize, 600) + PADDING;
|
|
7015
7138
|
const sampleSize = Math.min(100, data.length);
|
|
7016
7139
|
let maxDataWidth = 0;
|
|
7017
7140
|
for (let i = 0; i < sampleSize; i++) {
|
|
7018
7141
|
const val = data[i][col.key];
|
|
7019
7142
|
const text = val == null ? "" : String(val);
|
|
7020
|
-
const width =
|
|
7143
|
+
const width = estimateTextWidth10(text, fontSize, 400) + PADDING;
|
|
7021
7144
|
if (width > maxDataWidth) maxDataWidth = width;
|
|
7022
7145
|
}
|
|
7023
7146
|
return Math.max(MIN_WIDTH, headerWidth, maxDataWidth);
|
|
@@ -7427,7 +7550,8 @@ function compileChart(spec, options) {
|
|
|
7427
7550
|
}
|
|
7428
7551
|
let chartSpec = normalized;
|
|
7429
7552
|
const breakpoint = getBreakpoint(options.width);
|
|
7430
|
-
const
|
|
7553
|
+
const heightClass = getHeightClass(options.height);
|
|
7554
|
+
const strategy = getLayoutStrategy(breakpoint, heightClass);
|
|
7431
7555
|
const rawSpec = spec;
|
|
7432
7556
|
const overrides = rawSpec.overrides;
|
|
7433
7557
|
if (overrides?.[breakpoint]) {
|
|
@@ -7478,7 +7602,7 @@ function compileChart(spec, options) {
|
|
|
7478
7602
|
height: options.height
|
|
7479
7603
|
};
|
|
7480
7604
|
const legendLayout = computeLegend(chartSpec, strategy, theme, preliminaryArea);
|
|
7481
|
-
const dims = computeDimensions(chartSpec, options, legendLayout, theme);
|
|
7605
|
+
const dims = computeDimensions(chartSpec, options, legendLayout, theme, strategy);
|
|
7482
7606
|
const chartArea = dims.chartArea;
|
|
7483
7607
|
const legendArea = { ...chartArea };
|
|
7484
7608
|
if (legendLayout.entries.length > 0) {
|