@opendata-ai/openchart-engine 6.23.1 → 6.24.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 +104 -68
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/axes.test.ts +25 -4
- package/src/__tests__/dimensions.test.ts +48 -0
- package/src/__tests__/legend.test.ts +52 -25
- package/src/charts/dot/__tests__/compute.test.ts +31 -0
- package/src/charts/dot/compute.ts +6 -1
- package/src/compile.ts +5 -3
- package/src/layout/axes.ts +10 -5
- package/src/layout/dimensions.ts +15 -4
- package/src/legend/compute.ts +35 -9
- package/src/legend/wrap.ts +13 -2
- package/src/sankey/compile-sankey.ts +1 -0
package/dist/index.js
CHANGED
|
@@ -1489,7 +1489,10 @@ function computeDotMarks(spec, scales, _chartArea, _strategy) {
|
|
|
1489
1489
|
return [];
|
|
1490
1490
|
}
|
|
1491
1491
|
const bandwidth = yScale.bandwidth();
|
|
1492
|
-
const
|
|
1492
|
+
const [rangeStart, rangeEnd] = xScale.range();
|
|
1493
|
+
const rangeMin = Math.min(rangeStart, rangeEnd);
|
|
1494
|
+
const rangeMax = Math.max(rangeStart, rangeEnd);
|
|
1495
|
+
const baseline = Math.max(rangeMin, Math.min(rangeMax, xScale(0)));
|
|
1493
1496
|
const colorEnc = encoding.color && "field" in encoding.color ? encoding.color : void 0;
|
|
1494
1497
|
const isSequentialColor = colorEnc?.type === "quantitative";
|
|
1495
1498
|
const colorField = isSequentialColor ? void 0 : colorEnc?.field;
|
|
@@ -8117,7 +8120,8 @@ function computeAxes(scales, chartArea, strategy, theme, measureText) {
|
|
|
8117
8120
|
position: t.position,
|
|
8118
8121
|
major: true
|
|
8119
8122
|
}));
|
|
8120
|
-
const
|
|
8123
|
+
const hasExplicitValues = !!axisConfig?.values;
|
|
8124
|
+
const shouldThin = scales.x.type !== "band" && !hasExplicitValues;
|
|
8121
8125
|
let ticks2;
|
|
8122
8126
|
if (!shouldThin) {
|
|
8123
8127
|
ticks2 = allTicks;
|
|
@@ -8179,9 +8183,9 @@ function computeAxes(scales, chartArea, strategy, theme, measureText) {
|
|
|
8179
8183
|
} else {
|
|
8180
8184
|
allTicks = continuousTicks(scales.y, yDensity, yTargetCount);
|
|
8181
8185
|
}
|
|
8182
|
-
const
|
|
8186
|
+
const shouldThinY = scales.y.type !== "band" && !axisConfig?.values;
|
|
8183
8187
|
let ticks2;
|
|
8184
|
-
if (!
|
|
8188
|
+
if (!shouldThinY) {
|
|
8185
8189
|
ticks2 = allTicks;
|
|
8186
8190
|
} else if (isContinuousY) {
|
|
8187
8191
|
ticks2 = fitContinuousTicks(
|
|
@@ -8227,7 +8231,51 @@ function computeAxes(scales, chartArea, strategy, theme, measureText) {
|
|
|
8227
8231
|
}
|
|
8228
8232
|
|
|
8229
8233
|
// src/layout/dimensions.ts
|
|
8230
|
-
import { computeChrome as computeChrome2, estimateTextWidth as
|
|
8234
|
+
import { computeChrome as computeChrome2, estimateTextWidth as estimateTextWidth9 } from "@opendata-ai/openchart-core";
|
|
8235
|
+
|
|
8236
|
+
// src/legend/wrap.ts
|
|
8237
|
+
import { COMPACT_WIDTH, estimateTextWidth as estimateTextWidth8 } from "@opendata-ai/openchart-core";
|
|
8238
|
+
var SWATCH_SIZE2 = 12;
|
|
8239
|
+
var SWATCH_GAP2 = 6;
|
|
8240
|
+
var ENTRY_GAP2 = 16;
|
|
8241
|
+
var ENTRY_GAP_COMPACT = 10;
|
|
8242
|
+
var LEGEND_GAP = 4;
|
|
8243
|
+
function legendGap(width) {
|
|
8244
|
+
return width < COMPACT_WIDTH ? 0 : LEGEND_GAP;
|
|
8245
|
+
}
|
|
8246
|
+
function measureLegendWrap(entries, maxWidth, labelStyle, maxRows, entryGap = ENTRY_GAP2) {
|
|
8247
|
+
if (entries.length === 0) {
|
|
8248
|
+
return { rowCount: 0, fittingCount: 0, rowWidths: [] };
|
|
8249
|
+
}
|
|
8250
|
+
let rowCount = 1;
|
|
8251
|
+
let rowWidth = 0;
|
|
8252
|
+
const rowWidths = [];
|
|
8253
|
+
let fittingCount = entries.length;
|
|
8254
|
+
let fittingCountLocked = false;
|
|
8255
|
+
for (let i = 0; i < entries.length; i++) {
|
|
8256
|
+
const labelWidth = estimateTextWidth8(
|
|
8257
|
+
entries[i].label,
|
|
8258
|
+
labelStyle.fontSize,
|
|
8259
|
+
labelStyle.fontWeight
|
|
8260
|
+
);
|
|
8261
|
+
const entryWidth = SWATCH_SIZE2 + SWATCH_GAP2 + labelWidth + entryGap;
|
|
8262
|
+
if (rowWidth + entryWidth > maxWidth && rowWidth > 0) {
|
|
8263
|
+
rowWidths.push(rowWidth);
|
|
8264
|
+
rowCount++;
|
|
8265
|
+
rowWidth = entryWidth;
|
|
8266
|
+
if (!fittingCountLocked && maxRows != null && rowCount > maxRows) {
|
|
8267
|
+
fittingCount = i;
|
|
8268
|
+
fittingCountLocked = true;
|
|
8269
|
+
}
|
|
8270
|
+
} else {
|
|
8271
|
+
rowWidth += entryWidth;
|
|
8272
|
+
}
|
|
8273
|
+
}
|
|
8274
|
+
rowWidths.push(rowWidth);
|
|
8275
|
+
return { rowCount, fittingCount, rowWidths };
|
|
8276
|
+
}
|
|
8277
|
+
|
|
8278
|
+
// src/layout/dimensions.ts
|
|
8231
8279
|
function chromeToInput(chrome) {
|
|
8232
8280
|
return {
|
|
8233
8281
|
title: chrome.title,
|
|
@@ -8276,7 +8324,7 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
|
|
|
8276
8324
|
if (xField) {
|
|
8277
8325
|
for (const row of spec.data) {
|
|
8278
8326
|
const label = String(row[xField] ?? "");
|
|
8279
|
-
const w =
|
|
8327
|
+
const w = estimateTextWidth9(label, theme.fonts.sizes.axisTick, theme.fonts.weights.normal);
|
|
8280
8328
|
if (w > maxLabelWidth) maxLabelWidth = w;
|
|
8281
8329
|
}
|
|
8282
8330
|
}
|
|
@@ -8304,7 +8352,7 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
|
|
|
8304
8352
|
const label = String(row[colorField] ?? "");
|
|
8305
8353
|
if (!seen.has(label)) {
|
|
8306
8354
|
seen.add(label);
|
|
8307
|
-
const w =
|
|
8355
|
+
const w = estimateTextWidth9(label, 11, 600);
|
|
8308
8356
|
if (w > maxLabelWidth) maxLabelWidth = w;
|
|
8309
8357
|
}
|
|
8310
8358
|
}
|
|
@@ -8324,7 +8372,7 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
|
|
|
8324
8372
|
const maxXStr = String(maxX);
|
|
8325
8373
|
for (const ann of spec.annotations) {
|
|
8326
8374
|
if (ann.type === "text" && String(ann.x) === maxXStr) {
|
|
8327
|
-
const textWidth =
|
|
8375
|
+
const textWidth = estimateTextWidth9(ann.text, ann.fontSize ?? 11, ann.fontWeight ?? 600);
|
|
8328
8376
|
const dx = ann.offset?.dx ?? 0;
|
|
8329
8377
|
const anchor = ann.anchor ?? "auto";
|
|
8330
8378
|
const baseRightExtent = anchor === "left" ? textWidth : (
|
|
@@ -8348,11 +8396,15 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
|
|
|
8348
8396
|
let maxLabelWidth = 0;
|
|
8349
8397
|
for (const row of spec.data) {
|
|
8350
8398
|
const label = String(row[yField] ?? "");
|
|
8351
|
-
const w =
|
|
8399
|
+
const w = estimateTextWidth9(label, theme.fonts.sizes.axisTick, theme.fonts.weights.normal);
|
|
8352
8400
|
if (w > maxLabelWidth) maxLabelWidth = w;
|
|
8353
8401
|
}
|
|
8354
8402
|
if (maxLabelWidth > 0) {
|
|
8355
|
-
|
|
8403
|
+
const labelGap = width < 500 ? 8 : 12;
|
|
8404
|
+
const maxLeftFraction = width < 400 ? 0.45 : width < 600 ? 0.55 : 1;
|
|
8405
|
+
const maxLeftReserved = Math.floor(width * maxLeftFraction);
|
|
8406
|
+
const reserved = Math.min(padding + maxLabelWidth + labelGap, maxLeftReserved);
|
|
8407
|
+
margins.left = Math.max(margins.left, reserved);
|
|
8356
8408
|
}
|
|
8357
8409
|
} else if (encoding.y.type === "quantitative" || encoding.y.type === "temporal") {
|
|
8358
8410
|
const yField = encoding.y.field;
|
|
@@ -8380,7 +8432,7 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
|
|
|
8380
8432
|
}
|
|
8381
8433
|
const negPrefix = spec.data.some((r) => Number(r[yField]) < 0) ? "-" : "";
|
|
8382
8434
|
const labelEst = negPrefix + sampleLabel;
|
|
8383
|
-
const labelWidth =
|
|
8435
|
+
const labelWidth = estimateTextWidth9(
|
|
8384
8436
|
labelEst,
|
|
8385
8437
|
theme.fonts.sizes.axisTick,
|
|
8386
8438
|
theme.fonts.weights.normal
|
|
@@ -8394,12 +8446,13 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
|
|
|
8394
8446
|
margins.left = Math.max(margins.left, padding + rotatedLabelMargin);
|
|
8395
8447
|
}
|
|
8396
8448
|
if (legendLayout.entries.length > 0) {
|
|
8449
|
+
const gap = legendGap(width);
|
|
8397
8450
|
if (legendLayout.position === "right" || legendLayout.position === "bottom-right") {
|
|
8398
8451
|
margins.right += legendLayout.bounds.width + 8;
|
|
8399
8452
|
} else if (legendLayout.position === "top") {
|
|
8400
|
-
margins.top += legendLayout.bounds.height +
|
|
8453
|
+
margins.top += legendLayout.bounds.height + gap;
|
|
8401
8454
|
} else if (legendLayout.position === "bottom") {
|
|
8402
|
-
margins.bottom += legendLayout.bounds.height +
|
|
8455
|
+
margins.bottom += legendLayout.bounds.height + gap;
|
|
8403
8456
|
}
|
|
8404
8457
|
}
|
|
8405
8458
|
let chartArea = {
|
|
@@ -8425,7 +8478,8 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
|
|
|
8425
8478
|
const newBottom = padding + fallbackChrome.bottomHeight + xAxisHeight;
|
|
8426
8479
|
const bottomDelta = margins.bottom - newBottom;
|
|
8427
8480
|
if (topDelta > 0 || bottomDelta > 0) {
|
|
8428
|
-
|
|
8481
|
+
const gap = legendGap(width);
|
|
8482
|
+
margins.top = newTop + (legendLayout.entries.length > 0 && legendLayout.position === "top" ? legendLayout.bounds.height + gap : 0);
|
|
8429
8483
|
margins.bottom = newBottom;
|
|
8430
8484
|
chartArea = {
|
|
8431
8485
|
x: margins.left,
|
|
@@ -8894,46 +8948,7 @@ function computeScales(spec, chartArea, data) {
|
|
|
8894
8948
|
}
|
|
8895
8949
|
|
|
8896
8950
|
// src/legend/compute.ts
|
|
8897
|
-
import { BRAND_RESERVE_WIDTH as BRAND_RESERVE_WIDTH2, estimateTextWidth as estimateTextWidth10 } from "@opendata-ai/openchart-core";
|
|
8898
|
-
|
|
8899
|
-
// src/legend/wrap.ts
|
|
8900
|
-
import { estimateTextWidth as estimateTextWidth9 } from "@opendata-ai/openchart-core";
|
|
8901
|
-
var SWATCH_SIZE2 = 12;
|
|
8902
|
-
var SWATCH_GAP2 = 6;
|
|
8903
|
-
var ENTRY_GAP2 = 16;
|
|
8904
|
-
function measureLegendWrap(entries, maxWidth, labelStyle, maxRows) {
|
|
8905
|
-
if (entries.length === 0) {
|
|
8906
|
-
return { rowCount: 0, fittingCount: 0, rowWidths: [] };
|
|
8907
|
-
}
|
|
8908
|
-
let rowCount = 1;
|
|
8909
|
-
let rowWidth = 0;
|
|
8910
|
-
const rowWidths = [];
|
|
8911
|
-
let fittingCount = entries.length;
|
|
8912
|
-
let fittingCountLocked = false;
|
|
8913
|
-
for (let i = 0; i < entries.length; i++) {
|
|
8914
|
-
const labelWidth = estimateTextWidth9(
|
|
8915
|
-
entries[i].label,
|
|
8916
|
-
labelStyle.fontSize,
|
|
8917
|
-
labelStyle.fontWeight
|
|
8918
|
-
);
|
|
8919
|
-
const entryWidth = SWATCH_SIZE2 + SWATCH_GAP2 + labelWidth + ENTRY_GAP2;
|
|
8920
|
-
if (rowWidth + entryWidth > maxWidth && rowWidth > 0) {
|
|
8921
|
-
rowWidths.push(rowWidth);
|
|
8922
|
-
rowCount++;
|
|
8923
|
-
rowWidth = entryWidth;
|
|
8924
|
-
if (!fittingCountLocked && maxRows != null && rowCount > maxRows) {
|
|
8925
|
-
fittingCount = i;
|
|
8926
|
-
fittingCountLocked = true;
|
|
8927
|
-
}
|
|
8928
|
-
} else {
|
|
8929
|
-
rowWidth += entryWidth;
|
|
8930
|
-
}
|
|
8931
|
-
}
|
|
8932
|
-
rowWidths.push(rowWidth);
|
|
8933
|
-
return { rowCount, fittingCount, rowWidths };
|
|
8934
|
-
}
|
|
8935
|
-
|
|
8936
|
-
// src/legend/compute.ts
|
|
8951
|
+
import { BRAND_RESERVE_WIDTH as BRAND_RESERVE_WIDTH2, COMPACT_WIDTH as COMPACT_WIDTH2, estimateTextWidth as estimateTextWidth10 } from "@opendata-ai/openchart-core";
|
|
8937
8952
|
var LEGEND_PADDING = 8;
|
|
8938
8953
|
var LEGEND_RIGHT_WIDTH = 120;
|
|
8939
8954
|
var RIGHT_LEGEND_MAX_HEIGHT_RATIO = 0.4;
|
|
@@ -8955,11 +8970,15 @@ function extractColorEntries(spec, theme) {
|
|
|
8955
8970
|
if (!colorEnc) return [];
|
|
8956
8971
|
if ("condition" in colorEnc) return [];
|
|
8957
8972
|
if (colorEnc.type === "quantitative") return [];
|
|
8958
|
-
const
|
|
8973
|
+
const dataValues = [...new Set(spec.data.map((d) => String(d[colorEnc.field])))];
|
|
8959
8974
|
const explicitDomain = colorEnc.scale?.domain;
|
|
8960
8975
|
const explicitRange = colorEnc.scale?.range;
|
|
8961
8976
|
const palette = explicitRange ?? theme.colors.categorical;
|
|
8962
8977
|
const shape = swatchShapeForType(spec.markType);
|
|
8978
|
+
const uniqueValues = explicitDomain ? [
|
|
8979
|
+
...explicitDomain.filter((v) => dataValues.includes(v)),
|
|
8980
|
+
...dataValues.filter((v) => !explicitDomain.includes(v))
|
|
8981
|
+
] : dataValues;
|
|
8963
8982
|
return uniqueValues.map((value2, i) => {
|
|
8964
8983
|
let colorIndex = i;
|
|
8965
8984
|
if (explicitDomain && explicitRange) {
|
|
@@ -9079,7 +9098,10 @@ function computeLegend(spec, strategy, theme, chartArea, watermark = true) {
|
|
|
9079
9098
|
};
|
|
9080
9099
|
}
|
|
9081
9100
|
const reserveBrand = watermark && resolvedPosition === "bottom";
|
|
9082
|
-
const
|
|
9101
|
+
const isCompact = chartArea.width < COMPACT_WIDTH2;
|
|
9102
|
+
const effectivePadding = isCompact ? 2 : LEGEND_PADDING;
|
|
9103
|
+
const effectiveEntryGap = isCompact ? ENTRY_GAP_COMPACT : ENTRY_GAP2;
|
|
9104
|
+
const availableWidth = chartArea.width - effectivePadding * 2 - (reserveBrand ? BRAND_RESERVE_WIDTH2 : 0);
|
|
9083
9105
|
if (spec.legend?.symbolLimit != null) {
|
|
9084
9106
|
const limit = Math.max(1, spec.legend.symbolLimit);
|
|
9085
9107
|
if (limit < entries.length) {
|
|
@@ -9087,17 +9109,29 @@ function computeLegend(spec, strategy, theme, chartArea, watermark = true) {
|
|
|
9087
9109
|
}
|
|
9088
9110
|
}
|
|
9089
9111
|
const maxRows = spec.legend?.maxRows != null ? Math.max(1, spec.legend.maxRows) : spec.legend?.columns != null ? Math.ceil(entries.length / spec.legend.columns) : TOP_LEGEND_MAX_ROWS;
|
|
9090
|
-
const { fittingCount } = measureLegendWrap(
|
|
9112
|
+
const { fittingCount } = measureLegendWrap(
|
|
9113
|
+
entries,
|
|
9114
|
+
availableWidth,
|
|
9115
|
+
labelStyle,
|
|
9116
|
+
maxRows,
|
|
9117
|
+
effectiveEntryGap
|
|
9118
|
+
);
|
|
9091
9119
|
if (fittingCount < entries.length) {
|
|
9092
9120
|
entries = truncateEntries(entries, fittingCount);
|
|
9093
9121
|
}
|
|
9094
9122
|
const totalWidth = entries.reduce((sum2, entry) => {
|
|
9095
9123
|
const labelWidth = estimateTextWidth10(entry.label, labelStyle.fontSize, labelStyle.fontWeight);
|
|
9096
|
-
return sum2 + SWATCH_SIZE2 + SWATCH_GAP2 + labelWidth +
|
|
9124
|
+
return sum2 + SWATCH_SIZE2 + SWATCH_GAP2 + labelWidth + effectiveEntryGap;
|
|
9097
9125
|
}, 0);
|
|
9098
|
-
const { rowCount } = measureLegendWrap(
|
|
9126
|
+
const { rowCount } = measureLegendWrap(
|
|
9127
|
+
entries,
|
|
9128
|
+
availableWidth,
|
|
9129
|
+
labelStyle,
|
|
9130
|
+
void 0,
|
|
9131
|
+
effectiveEntryGap
|
|
9132
|
+
);
|
|
9099
9133
|
const rowHeight = SWATCH_SIZE2 + 4;
|
|
9100
|
-
const legendHeight = rowCount * rowHeight +
|
|
9134
|
+
const legendHeight = rowCount * rowHeight + effectivePadding * 2;
|
|
9101
9135
|
const offsetDx = spec.legend?.offset?.dx ?? 0;
|
|
9102
9136
|
const offsetDy = spec.legend?.offset?.dy ?? 0;
|
|
9103
9137
|
return {
|
|
@@ -9112,7 +9146,7 @@ function computeLegend(spec, strategy, theme, chartArea, watermark = true) {
|
|
|
9112
9146
|
labelStyle,
|
|
9113
9147
|
swatchSize: SWATCH_SIZE2,
|
|
9114
9148
|
swatchGap: SWATCH_GAP2,
|
|
9115
|
-
entryGap:
|
|
9149
|
+
entryGap: effectiveEntryGap
|
|
9116
9150
|
};
|
|
9117
9151
|
}
|
|
9118
9152
|
|
|
@@ -9766,12 +9800,12 @@ function compileSankey(spec, options) {
|
|
|
9766
9800
|
theme,
|
|
9767
9801
|
fullArea
|
|
9768
9802
|
);
|
|
9769
|
-
const
|
|
9803
|
+
const legendGap2 = legend.entries.length > 0 ? 4 : 0;
|
|
9770
9804
|
const area = {
|
|
9771
9805
|
x: fullArea.x,
|
|
9772
|
-
y: fullArea.y + legend.bounds.height +
|
|
9806
|
+
y: fullArea.y + legend.bounds.height + legendGap2,
|
|
9773
9807
|
width: fullArea.width,
|
|
9774
|
-
height: fullArea.height - legend.bounds.height -
|
|
9808
|
+
height: fullArea.height - legend.bounds.height - legendGap2
|
|
9775
9809
|
};
|
|
9776
9810
|
if (area.height <= 0) {
|
|
9777
9811
|
return emptyLayout(area, chrome, theme, options, watermark);
|
|
@@ -9929,7 +9963,8 @@ function compileSankey(spec, options) {
|
|
|
9929
9963
|
height: options.height
|
|
9930
9964
|
},
|
|
9931
9965
|
animation: resolvedAnimation,
|
|
9932
|
-
watermark
|
|
9966
|
+
watermark,
|
|
9967
|
+
measureText: options.measureText
|
|
9933
9968
|
};
|
|
9934
9969
|
}
|
|
9935
9970
|
function buildSankeyLegend(nodeColorMap, colorField, data, sourceField, targetField, theme, area) {
|
|
@@ -11320,13 +11355,14 @@ function compileChart(spec, options) {
|
|
|
11320
11355
|
const chartArea = dims.chartArea;
|
|
11321
11356
|
const legendArea = { ...chartArea };
|
|
11322
11357
|
if (legendLayout.entries.length > 0) {
|
|
11358
|
+
const gap = legendGap(options.width);
|
|
11323
11359
|
switch (legendLayout.position) {
|
|
11324
11360
|
case "top":
|
|
11325
|
-
legendArea.y -= legendLayout.bounds.height +
|
|
11326
|
-
legendArea.height += legendLayout.bounds.height +
|
|
11361
|
+
legendArea.y -= legendLayout.bounds.height + gap;
|
|
11362
|
+
legendArea.height += legendLayout.bounds.height + gap;
|
|
11327
11363
|
break;
|
|
11328
11364
|
case "bottom":
|
|
11329
|
-
legendArea.height += legendLayout.bounds.height +
|
|
11365
|
+
legendArea.height += legendLayout.bounds.height + gap;
|
|
11330
11366
|
break;
|
|
11331
11367
|
case "right":
|
|
11332
11368
|
case "bottom-right":
|