@opendata-ai/openchart-engine 6.23.1 → 6.24.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.js +148 -84
- 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 +63 -25
- package/src/annotations/compute.ts +5 -4
- package/src/annotations/resolve-refline.ts +4 -2
- package/src/charts/bar/labels.ts +26 -9
- package/src/charts/dot/__tests__/compute.test.ts +31 -0
- package/src/charts/dot/compute.ts +6 -1
- package/src/charts/line/__tests__/compute.test.ts +28 -0
- package/src/charts/line/area.ts +12 -2
- package/src/compile.ts +5 -3
- package/src/compiler/normalize.ts +2 -0
- package/src/layout/axes.ts +10 -5
- package/src/layout/dimensions.ts +22 -6
- package/src/legend/compute.ts +66 -26
- package/src/legend/wrap.ts +13 -2
- package/src/sankey/compile-sankey.ts +1 -0
package/dist/index.js
CHANGED
|
@@ -467,7 +467,9 @@ function resolveRefLineAnnotation(annotation, scales, chartArea, isDark) {
|
|
|
467
467
|
return null;
|
|
468
468
|
}
|
|
469
469
|
let strokeDasharray;
|
|
470
|
-
if (annotation.
|
|
470
|
+
if (annotation.strokeDash && annotation.strokeDash.length > 0) {
|
|
471
|
+
strokeDasharray = annotation.strokeDash.join(" ");
|
|
472
|
+
} else if (annotation.style === "dashed" || annotation.style === void 0) {
|
|
471
473
|
strokeDasharray = DEFAULT_REFLINE_DASH;
|
|
472
474
|
} else if (annotation.style === "dotted") {
|
|
473
475
|
strokeDasharray = "2 2";
|
|
@@ -549,11 +551,12 @@ function resolveRefLineAnnotation(annotation, scales, chartArea, isDark) {
|
|
|
549
551
|
|
|
550
552
|
// src/annotations/compute.ts
|
|
551
553
|
function computeAnnotations(spec, scales, chartArea, strategy, isDark = false, obstacles = [], svgDimensions) {
|
|
552
|
-
|
|
553
|
-
return [];
|
|
554
|
-
}
|
|
554
|
+
const isCompact = strategy.annotationPosition === "tooltip-only";
|
|
555
555
|
const annotations = [];
|
|
556
556
|
for (const annotation of spec.annotations) {
|
|
557
|
+
if (isCompact && annotation.responsive !== false) {
|
|
558
|
+
continue;
|
|
559
|
+
}
|
|
557
560
|
let resolved = null;
|
|
558
561
|
switch (annotation.type) {
|
|
559
562
|
case "text":
|
|
@@ -979,6 +982,7 @@ function computeSimpleBars(data, valueField, categoryField, xScale, yScale, band
|
|
|
979
982
|
import {
|
|
980
983
|
buildD3Formatter,
|
|
981
984
|
estimateTextWidth as estimateTextWidth2,
|
|
985
|
+
findAccessibleColor,
|
|
982
986
|
getRepresentativeColor,
|
|
983
987
|
resolveCollisions
|
|
984
988
|
} from "@opendata-ai/openchart-core";
|
|
@@ -1047,21 +1051,35 @@ function computeBarLabels(marks, _chartArea, density = "auto", labelFormat, labe
|
|
|
1047
1051
|
const textHeight = LABEL_FONT_SIZE * 1.2;
|
|
1048
1052
|
const isStacked = mark.cornerRadius === 0;
|
|
1049
1053
|
const isInside = mark.width >= MIN_WIDTH_FOR_INSIDE_LABEL;
|
|
1054
|
+
const isNegative = Number.isFinite(rawNum) ? rawNum < 0 : false;
|
|
1055
|
+
const bgColor = getRepresentativeColor(mark.fill);
|
|
1050
1056
|
let anchorX;
|
|
1051
1057
|
let fill;
|
|
1052
1058
|
let textAnchor;
|
|
1053
1059
|
if (isStacked && isInside) {
|
|
1054
1060
|
anchorX = mark.x + mark.width / 2;
|
|
1055
|
-
fill = "#ffffff";
|
|
1061
|
+
fill = findAccessibleColor("#ffffff", bgColor, 4.5);
|
|
1056
1062
|
textAnchor = "middle";
|
|
1057
1063
|
} else if (isInside) {
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1064
|
+
if (isNegative) {
|
|
1065
|
+
anchorX = mark.x + LABEL_PADDING;
|
|
1066
|
+
fill = findAccessibleColor("#ffffff", bgColor, 4.5);
|
|
1067
|
+
textAnchor = "start";
|
|
1068
|
+
} else {
|
|
1069
|
+
anchorX = mark.x + mark.width - LABEL_PADDING;
|
|
1070
|
+
fill = findAccessibleColor("#ffffff", bgColor, 4.5);
|
|
1071
|
+
textAnchor = "end";
|
|
1072
|
+
}
|
|
1061
1073
|
} else {
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1074
|
+
if (isNegative) {
|
|
1075
|
+
anchorX = mark.x - LABEL_PADDING;
|
|
1076
|
+
fill = getRepresentativeColor(mark.fill);
|
|
1077
|
+
textAnchor = "end";
|
|
1078
|
+
} else {
|
|
1079
|
+
anchorX = mark.x + mark.width + LABEL_PADDING;
|
|
1080
|
+
fill = getRepresentativeColor(mark.fill);
|
|
1081
|
+
textAnchor = "start";
|
|
1082
|
+
}
|
|
1065
1083
|
}
|
|
1066
1084
|
const anchorY = mark.y + mark.height / 2;
|
|
1067
1085
|
const fits = !(isStacked && textWidth > mark.width - 2 * LABEL_PADDING);
|
|
@@ -1489,7 +1507,10 @@ function computeDotMarks(spec, scales, _chartArea, _strategy) {
|
|
|
1489
1507
|
return [];
|
|
1490
1508
|
}
|
|
1491
1509
|
const bandwidth = yScale.bandwidth();
|
|
1492
|
-
const
|
|
1510
|
+
const [rangeStart, rangeEnd] = xScale.range();
|
|
1511
|
+
const rangeMin = Math.min(rangeStart, rangeEnd);
|
|
1512
|
+
const rangeMax = Math.max(rangeStart, rangeEnd);
|
|
1513
|
+
const baseline = Math.max(rangeMin, Math.min(rangeMax, xScale(0)));
|
|
1493
1514
|
const colorEnc = encoding.color && "field" in encoding.color ? encoding.color : void 0;
|
|
1494
1515
|
const isSequentialColor = colorEnc?.type === "quantitative";
|
|
1495
1516
|
const colorField = isSequentialColor ? void 0 : colorEnc?.field;
|
|
@@ -2659,14 +2680,16 @@ function computeSingleArea(spec, scales, _chartArea) {
|
|
|
2659
2680
|
const color2 = getColor(scales, seriesKey);
|
|
2660
2681
|
const sortedRows = xChannel.type === "nominal" || xChannel.type === "ordinal" ? rows : sortByField(rows, xChannel.field);
|
|
2661
2682
|
const validPoints = [];
|
|
2683
|
+
const y2Channel = encoding.y2;
|
|
2662
2684
|
for (const row of sortedRows) {
|
|
2663
2685
|
const xVal = scaleValue(scales.x.scale, scales.x.type, row[xChannel.field]);
|
|
2664
2686
|
const yVal = scaleValue(scales.y.scale, scales.y.type, row[yChannel.field]);
|
|
2665
2687
|
if (xVal === null || yVal === null) continue;
|
|
2688
|
+
const yBottomVal = y2Channel && row[y2Channel.field] != null ? scaleValue(scales.y.scale, scales.y.type, row[y2Channel.field]) : null;
|
|
2666
2689
|
validPoints.push({
|
|
2667
2690
|
x: xVal,
|
|
2668
2691
|
yTop: yVal,
|
|
2669
|
-
yBottom: baselineY,
|
|
2692
|
+
yBottom: yBottomVal ?? baselineY,
|
|
2670
2693
|
row
|
|
2671
2694
|
});
|
|
2672
2695
|
}
|
|
@@ -2680,6 +2703,7 @@ function computeSingleArea(spec, scales, _chartArea) {
|
|
|
2680
2703
|
const bottomPoints = validPoints.map((p) => ({ x: p.x, y: p.yBottom }));
|
|
2681
2704
|
const ariaLabel = seriesKey === "__default__" ? `Area with ${validPoints.length} data points` : `${seriesKey}: area with ${validPoints.length} data points`;
|
|
2682
2705
|
const aria = { label: ariaLabel };
|
|
2706
|
+
const fillOpacity = y2Channel ? 0.25 : DEFAULT_FILL_OPACITY;
|
|
2683
2707
|
marks.push({
|
|
2684
2708
|
type: "area",
|
|
2685
2709
|
topPoints,
|
|
@@ -2687,7 +2711,7 @@ function computeSingleArea(spec, scales, _chartArea) {
|
|
|
2687
2711
|
path: pathStr,
|
|
2688
2712
|
topPath: topPathStr,
|
|
2689
2713
|
fill: color2,
|
|
2690
|
-
fillOpacity
|
|
2714
|
+
fillOpacity,
|
|
2691
2715
|
stroke: getRepresentativeColor4(color2),
|
|
2692
2716
|
strokeWidth: 2,
|
|
2693
2717
|
seriesKey: seriesKey === "__default__" ? void 0 : seriesKey,
|
|
@@ -6604,8 +6628,10 @@ function normalizeAnnotations(annotations) {
|
|
|
6604
6628
|
fill: ann.fill ?? "#000000"
|
|
6605
6629
|
};
|
|
6606
6630
|
case "refline":
|
|
6631
|
+
case "rule":
|
|
6607
6632
|
return {
|
|
6608
6633
|
...ann,
|
|
6634
|
+
type: "refline",
|
|
6609
6635
|
style: ann.style ?? "dashed",
|
|
6610
6636
|
strokeWidth: ann.strokeWidth ?? 1,
|
|
6611
6637
|
stroke: ann.stroke ?? "#666666",
|
|
@@ -8117,7 +8143,8 @@ function computeAxes(scales, chartArea, strategy, theme, measureText) {
|
|
|
8117
8143
|
position: t.position,
|
|
8118
8144
|
major: true
|
|
8119
8145
|
}));
|
|
8120
|
-
const
|
|
8146
|
+
const hasExplicitValues = !!axisConfig?.values;
|
|
8147
|
+
const shouldThin = scales.x.type !== "band" && !hasExplicitValues;
|
|
8121
8148
|
let ticks2;
|
|
8122
8149
|
if (!shouldThin) {
|
|
8123
8150
|
ticks2 = allTicks;
|
|
@@ -8179,9 +8206,9 @@ function computeAxes(scales, chartArea, strategy, theme, measureText) {
|
|
|
8179
8206
|
} else {
|
|
8180
8207
|
allTicks = continuousTicks(scales.y, yDensity, yTargetCount);
|
|
8181
8208
|
}
|
|
8182
|
-
const
|
|
8209
|
+
const shouldThinY = scales.y.type !== "band" && !axisConfig?.values;
|
|
8183
8210
|
let ticks2;
|
|
8184
|
-
if (!
|
|
8211
|
+
if (!shouldThinY) {
|
|
8185
8212
|
ticks2 = allTicks;
|
|
8186
8213
|
} else if (isContinuousY) {
|
|
8187
8214
|
ticks2 = fitContinuousTicks(
|
|
@@ -8227,7 +8254,51 @@ function computeAxes(scales, chartArea, strategy, theme, measureText) {
|
|
|
8227
8254
|
}
|
|
8228
8255
|
|
|
8229
8256
|
// src/layout/dimensions.ts
|
|
8230
|
-
import { computeChrome as computeChrome2, estimateTextWidth as
|
|
8257
|
+
import { computeChrome as computeChrome2, estimateTextWidth as estimateTextWidth9 } from "@opendata-ai/openchart-core";
|
|
8258
|
+
|
|
8259
|
+
// src/legend/wrap.ts
|
|
8260
|
+
import { COMPACT_WIDTH, estimateTextWidth as estimateTextWidth8 } from "@opendata-ai/openchart-core";
|
|
8261
|
+
var SWATCH_SIZE2 = 12;
|
|
8262
|
+
var SWATCH_GAP2 = 6;
|
|
8263
|
+
var ENTRY_GAP2 = 16;
|
|
8264
|
+
var ENTRY_GAP_COMPACT = 10;
|
|
8265
|
+
var LEGEND_GAP = 4;
|
|
8266
|
+
function legendGap(width) {
|
|
8267
|
+
return width < COMPACT_WIDTH ? 0 : LEGEND_GAP;
|
|
8268
|
+
}
|
|
8269
|
+
function measureLegendWrap(entries, maxWidth, labelStyle, maxRows, entryGap = ENTRY_GAP2) {
|
|
8270
|
+
if (entries.length === 0) {
|
|
8271
|
+
return { rowCount: 0, fittingCount: 0, rowWidths: [] };
|
|
8272
|
+
}
|
|
8273
|
+
let rowCount = 1;
|
|
8274
|
+
let rowWidth = 0;
|
|
8275
|
+
const rowWidths = [];
|
|
8276
|
+
let fittingCount = entries.length;
|
|
8277
|
+
let fittingCountLocked = false;
|
|
8278
|
+
for (let i = 0; i < entries.length; i++) {
|
|
8279
|
+
const labelWidth = estimateTextWidth8(
|
|
8280
|
+
entries[i].label,
|
|
8281
|
+
labelStyle.fontSize,
|
|
8282
|
+
labelStyle.fontWeight
|
|
8283
|
+
);
|
|
8284
|
+
const entryWidth = SWATCH_SIZE2 + SWATCH_GAP2 + labelWidth + entryGap;
|
|
8285
|
+
if (rowWidth + entryWidth > maxWidth && rowWidth > 0) {
|
|
8286
|
+
rowWidths.push(rowWidth);
|
|
8287
|
+
rowCount++;
|
|
8288
|
+
rowWidth = entryWidth;
|
|
8289
|
+
if (!fittingCountLocked && maxRows != null && rowCount > maxRows) {
|
|
8290
|
+
fittingCount = i;
|
|
8291
|
+
fittingCountLocked = true;
|
|
8292
|
+
}
|
|
8293
|
+
} else {
|
|
8294
|
+
rowWidth += entryWidth;
|
|
8295
|
+
}
|
|
8296
|
+
}
|
|
8297
|
+
rowWidths.push(rowWidth);
|
|
8298
|
+
return { rowCount, fittingCount, rowWidths };
|
|
8299
|
+
}
|
|
8300
|
+
|
|
8301
|
+
// src/layout/dimensions.ts
|
|
8231
8302
|
function chromeToInput(chrome) {
|
|
8232
8303
|
return {
|
|
8233
8304
|
title: chrome.title,
|
|
@@ -8276,7 +8347,7 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
|
|
|
8276
8347
|
if (xField) {
|
|
8277
8348
|
for (const row of spec.data) {
|
|
8278
8349
|
const label = String(row[xField] ?? "");
|
|
8279
|
-
const w =
|
|
8350
|
+
const w = estimateTextWidth9(label, theme.fonts.sizes.axisTick, theme.fonts.weights.normal);
|
|
8280
8351
|
if (w > maxLabelWidth) maxLabelWidth = w;
|
|
8281
8352
|
}
|
|
8282
8353
|
}
|
|
@@ -8294,7 +8365,8 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
|
|
|
8294
8365
|
left: padding + (isRadial ? padding : axisMargin)
|
|
8295
8366
|
};
|
|
8296
8367
|
const labelDensity = spec.labels.density;
|
|
8297
|
-
|
|
8368
|
+
const labelsHiddenByStrategy = strategy?.labelMode === "none";
|
|
8369
|
+
if ((spec.markType === "line" || spec.markType === "area") && labelDensity !== "none" && !labelsHiddenByStrategy) {
|
|
8298
8370
|
const colorEnc = encoding.color;
|
|
8299
8371
|
const colorField = colorEnc && "field" in colorEnc ? colorEnc.field : void 0;
|
|
8300
8372
|
if (colorField) {
|
|
@@ -8304,7 +8376,7 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
|
|
|
8304
8376
|
const label = String(row[colorField] ?? "");
|
|
8305
8377
|
if (!seen.has(label)) {
|
|
8306
8378
|
seen.add(label);
|
|
8307
|
-
const w =
|
|
8379
|
+
const w = estimateTextWidth9(label, 11, 600);
|
|
8308
8380
|
if (w > maxLabelWidth) maxLabelWidth = w;
|
|
8309
8381
|
}
|
|
8310
8382
|
}
|
|
@@ -8324,7 +8396,7 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
|
|
|
8324
8396
|
const maxXStr = String(maxX);
|
|
8325
8397
|
for (const ann of spec.annotations) {
|
|
8326
8398
|
if (ann.type === "text" && String(ann.x) === maxXStr) {
|
|
8327
|
-
const textWidth =
|
|
8399
|
+
const textWidth = estimateTextWidth9(ann.text, ann.fontSize ?? 11, ann.fontWeight ?? 600);
|
|
8328
8400
|
const dx = ann.offset?.dx ?? 0;
|
|
8329
8401
|
const anchor = ann.anchor ?? "auto";
|
|
8330
8402
|
const baseRightExtent = anchor === "left" ? textWidth : (
|
|
@@ -8348,11 +8420,15 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
|
|
|
8348
8420
|
let maxLabelWidth = 0;
|
|
8349
8421
|
for (const row of spec.data) {
|
|
8350
8422
|
const label = String(row[yField] ?? "");
|
|
8351
|
-
const w =
|
|
8423
|
+
const w = estimateTextWidth9(label, theme.fonts.sizes.axisTick, theme.fonts.weights.normal);
|
|
8352
8424
|
if (w > maxLabelWidth) maxLabelWidth = w;
|
|
8353
8425
|
}
|
|
8354
8426
|
if (maxLabelWidth > 0) {
|
|
8355
|
-
|
|
8427
|
+
const labelGap = width < 500 ? 8 : 12;
|
|
8428
|
+
const maxLeftFraction = width < 400 ? 0.45 : width < 600 ? 0.55 : 1;
|
|
8429
|
+
const maxLeftReserved = Math.floor(width * maxLeftFraction);
|
|
8430
|
+
const reserved = Math.min(padding + maxLabelWidth + labelGap, maxLeftReserved);
|
|
8431
|
+
margins.left = Math.max(margins.left, reserved);
|
|
8356
8432
|
}
|
|
8357
8433
|
} else if (encoding.y.type === "quantitative" || encoding.y.type === "temporal") {
|
|
8358
8434
|
const yField = encoding.y.field;
|
|
@@ -8380,7 +8456,7 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
|
|
|
8380
8456
|
}
|
|
8381
8457
|
const negPrefix = spec.data.some((r) => Number(r[yField]) < 0) ? "-" : "";
|
|
8382
8458
|
const labelEst = negPrefix + sampleLabel;
|
|
8383
|
-
const labelWidth =
|
|
8459
|
+
const labelWidth = estimateTextWidth9(
|
|
8384
8460
|
labelEst,
|
|
8385
8461
|
theme.fonts.sizes.axisTick,
|
|
8386
8462
|
theme.fonts.weights.normal
|
|
@@ -8394,12 +8470,13 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
|
|
|
8394
8470
|
margins.left = Math.max(margins.left, padding + rotatedLabelMargin);
|
|
8395
8471
|
}
|
|
8396
8472
|
if (legendLayout.entries.length > 0) {
|
|
8473
|
+
const gap = legendGap(width);
|
|
8397
8474
|
if (legendLayout.position === "right" || legendLayout.position === "bottom-right") {
|
|
8398
8475
|
margins.right += legendLayout.bounds.width + 8;
|
|
8399
8476
|
} else if (legendLayout.position === "top") {
|
|
8400
|
-
margins.top += legendLayout.bounds.height +
|
|
8477
|
+
margins.top += legendLayout.bounds.height + gap;
|
|
8401
8478
|
} else if (legendLayout.position === "bottom") {
|
|
8402
|
-
margins.bottom += legendLayout.bounds.height +
|
|
8479
|
+
margins.bottom += legendLayout.bounds.height + gap;
|
|
8403
8480
|
}
|
|
8404
8481
|
}
|
|
8405
8482
|
let chartArea = {
|
|
@@ -8425,7 +8502,8 @@ function computeDimensions(spec, options, legendLayout, theme, strategy, waterma
|
|
|
8425
8502
|
const newBottom = padding + fallbackChrome.bottomHeight + xAxisHeight;
|
|
8426
8503
|
const bottomDelta = margins.bottom - newBottom;
|
|
8427
8504
|
if (topDelta > 0 || bottomDelta > 0) {
|
|
8428
|
-
|
|
8505
|
+
const gap = legendGap(width);
|
|
8506
|
+
margins.top = newTop + (legendLayout.entries.length > 0 && legendLayout.position === "top" ? legendLayout.bounds.height + gap : 0);
|
|
8429
8507
|
margins.bottom = newBottom;
|
|
8430
8508
|
chartArea = {
|
|
8431
8509
|
x: margins.left,
|
|
@@ -8894,46 +8972,7 @@ function computeScales(spec, chartArea, data) {
|
|
|
8894
8972
|
}
|
|
8895
8973
|
|
|
8896
8974
|
// 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
|
|
8975
|
+
import { BRAND_RESERVE_WIDTH as BRAND_RESERVE_WIDTH2, COMPACT_WIDTH as COMPACT_WIDTH2, estimateTextWidth as estimateTextWidth10 } from "@opendata-ai/openchart-core";
|
|
8937
8976
|
var LEGEND_PADDING = 8;
|
|
8938
8977
|
var LEGEND_RIGHT_WIDTH = 120;
|
|
8939
8978
|
var RIGHT_LEGEND_MAX_HEIGHT_RATIO = 0.4;
|
|
@@ -8955,11 +8994,16 @@ function extractColorEntries(spec, theme) {
|
|
|
8955
8994
|
if (!colorEnc) return [];
|
|
8956
8995
|
if ("condition" in colorEnc) return [];
|
|
8957
8996
|
if (colorEnc.type === "quantitative") return [];
|
|
8958
|
-
const
|
|
8997
|
+
const dataValues = [...new Set(spec.data.map((d) => String(d[colorEnc.field])))];
|
|
8959
8998
|
const explicitDomain = colorEnc.scale?.domain;
|
|
8960
8999
|
const explicitRange = colorEnc.scale?.range;
|
|
8961
9000
|
const palette = explicitRange ?? theme.colors.categorical;
|
|
8962
9001
|
const shape = swatchShapeForType(spec.markType);
|
|
9002
|
+
const uniqueValues = explicitDomain ? [
|
|
9003
|
+
...explicitDomain.filter((v) => dataValues.includes(v)),
|
|
9004
|
+
...dataValues.filter((v) => !explicitDomain.includes(v))
|
|
9005
|
+
] : dataValues;
|
|
9006
|
+
const excludeSet = new Set(spec.legend?.exclude ?? []);
|
|
8963
9007
|
return uniqueValues.map((value2, i) => {
|
|
8964
9008
|
let colorIndex = i;
|
|
8965
9009
|
if (explicitDomain && explicitRange) {
|
|
@@ -8972,7 +9016,7 @@ function extractColorEntries(spec, theme) {
|
|
|
8972
9016
|
shape,
|
|
8973
9017
|
active: true
|
|
8974
9018
|
};
|
|
8975
|
-
});
|
|
9019
|
+
}).filter((entry) => !excludeSet.has(entry.label));
|
|
8976
9020
|
}
|
|
8977
9021
|
function truncateEntries(entries, maxCount) {
|
|
8978
9022
|
if (maxCount >= entries.length || maxCount <= 0) return entries;
|
|
@@ -9010,7 +9054,10 @@ function computeLegend(spec, strategy, theme, chartArea, watermark = true) {
|
|
|
9010
9054
|
const hasLabels = spec.labels.density !== "none";
|
|
9011
9055
|
const labelsWillRender = strategy.labelMode !== "none";
|
|
9012
9056
|
const hasColorEncoding = spec.encoding.color != null;
|
|
9013
|
-
const
|
|
9057
|
+
const userConfiguredLegend = spec.legend != null && Object.keys(spec.legend).some(
|
|
9058
|
+
(k) => k !== "show" || spec.legend[k] !== false
|
|
9059
|
+
);
|
|
9060
|
+
const legendNotForced = !userConfiguredLegend;
|
|
9014
9061
|
if (isLineOrArea && hasLabels && labelsWillRender && hasColorEncoding && legendNotForced) {
|
|
9015
9062
|
const isArea = spec.markType === "area";
|
|
9016
9063
|
const quantChannel = spec.encoding.y?.type === "quantitative" ? spec.encoding.y : spec.encoding.x;
|
|
@@ -9079,7 +9126,10 @@ function computeLegend(spec, strategy, theme, chartArea, watermark = true) {
|
|
|
9079
9126
|
};
|
|
9080
9127
|
}
|
|
9081
9128
|
const reserveBrand = watermark && resolvedPosition === "bottom";
|
|
9082
|
-
const
|
|
9129
|
+
const isCompact = chartArea.width < COMPACT_WIDTH2;
|
|
9130
|
+
const effectivePadding = isCompact ? 2 : LEGEND_PADDING;
|
|
9131
|
+
const effectiveEntryGap = isCompact ? ENTRY_GAP_COMPACT : ENTRY_GAP2;
|
|
9132
|
+
const availableWidth = chartArea.width - effectivePadding * 2 - (reserveBrand ? BRAND_RESERVE_WIDTH2 : 0);
|
|
9083
9133
|
if (spec.legend?.symbolLimit != null) {
|
|
9084
9134
|
const limit = Math.max(1, spec.legend.symbolLimit);
|
|
9085
9135
|
if (limit < entries.length) {
|
|
@@ -9087,17 +9137,29 @@ function computeLegend(spec, strategy, theme, chartArea, watermark = true) {
|
|
|
9087
9137
|
}
|
|
9088
9138
|
}
|
|
9089
9139
|
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(
|
|
9140
|
+
const { fittingCount } = measureLegendWrap(
|
|
9141
|
+
entries,
|
|
9142
|
+
availableWidth,
|
|
9143
|
+
labelStyle,
|
|
9144
|
+
maxRows,
|
|
9145
|
+
effectiveEntryGap
|
|
9146
|
+
);
|
|
9091
9147
|
if (fittingCount < entries.length) {
|
|
9092
9148
|
entries = truncateEntries(entries, fittingCount);
|
|
9093
9149
|
}
|
|
9094
9150
|
const totalWidth = entries.reduce((sum2, entry) => {
|
|
9095
9151
|
const labelWidth = estimateTextWidth10(entry.label, labelStyle.fontSize, labelStyle.fontWeight);
|
|
9096
|
-
return sum2 + SWATCH_SIZE2 + SWATCH_GAP2 + labelWidth +
|
|
9152
|
+
return sum2 + SWATCH_SIZE2 + SWATCH_GAP2 + labelWidth + effectiveEntryGap;
|
|
9097
9153
|
}, 0);
|
|
9098
|
-
const { rowCount } = measureLegendWrap(
|
|
9154
|
+
const { rowCount } = measureLegendWrap(
|
|
9155
|
+
entries,
|
|
9156
|
+
availableWidth,
|
|
9157
|
+
labelStyle,
|
|
9158
|
+
void 0,
|
|
9159
|
+
effectiveEntryGap
|
|
9160
|
+
);
|
|
9099
9161
|
const rowHeight = SWATCH_SIZE2 + 4;
|
|
9100
|
-
const legendHeight = rowCount * rowHeight +
|
|
9162
|
+
const legendHeight = rowCount * rowHeight + effectivePadding * 2;
|
|
9101
9163
|
const offsetDx = spec.legend?.offset?.dx ?? 0;
|
|
9102
9164
|
const offsetDy = spec.legend?.offset?.dy ?? 0;
|
|
9103
9165
|
return {
|
|
@@ -9112,7 +9174,7 @@ function computeLegend(spec, strategy, theme, chartArea, watermark = true) {
|
|
|
9112
9174
|
labelStyle,
|
|
9113
9175
|
swatchSize: SWATCH_SIZE2,
|
|
9114
9176
|
swatchGap: SWATCH_GAP2,
|
|
9115
|
-
entryGap:
|
|
9177
|
+
entryGap: effectiveEntryGap
|
|
9116
9178
|
};
|
|
9117
9179
|
}
|
|
9118
9180
|
|
|
@@ -9766,12 +9828,12 @@ function compileSankey(spec, options) {
|
|
|
9766
9828
|
theme,
|
|
9767
9829
|
fullArea
|
|
9768
9830
|
);
|
|
9769
|
-
const
|
|
9831
|
+
const legendGap2 = legend.entries.length > 0 ? 4 : 0;
|
|
9770
9832
|
const area = {
|
|
9771
9833
|
x: fullArea.x,
|
|
9772
|
-
y: fullArea.y + legend.bounds.height +
|
|
9834
|
+
y: fullArea.y + legend.bounds.height + legendGap2,
|
|
9773
9835
|
width: fullArea.width,
|
|
9774
|
-
height: fullArea.height - legend.bounds.height -
|
|
9836
|
+
height: fullArea.height - legend.bounds.height - legendGap2
|
|
9775
9837
|
};
|
|
9776
9838
|
if (area.height <= 0) {
|
|
9777
9839
|
return emptyLayout(area, chrome, theme, options, watermark);
|
|
@@ -9929,7 +9991,8 @@ function compileSankey(spec, options) {
|
|
|
9929
9991
|
height: options.height
|
|
9930
9992
|
},
|
|
9931
9993
|
animation: resolvedAnimation,
|
|
9932
|
-
watermark
|
|
9994
|
+
watermark,
|
|
9995
|
+
measureText: options.measureText
|
|
9933
9996
|
};
|
|
9934
9997
|
}
|
|
9935
9998
|
function buildSankeyLegend(nodeColorMap, colorField, data, sourceField, targetField, theme, area) {
|
|
@@ -11320,13 +11383,14 @@ function compileChart(spec, options) {
|
|
|
11320
11383
|
const chartArea = dims.chartArea;
|
|
11321
11384
|
const legendArea = { ...chartArea };
|
|
11322
11385
|
if (legendLayout.entries.length > 0) {
|
|
11386
|
+
const gap = legendGap(options.width);
|
|
11323
11387
|
switch (legendLayout.position) {
|
|
11324
11388
|
case "top":
|
|
11325
|
-
legendArea.y -= legendLayout.bounds.height +
|
|
11326
|
-
legendArea.height += legendLayout.bounds.height +
|
|
11389
|
+
legendArea.y -= legendLayout.bounds.height + gap;
|
|
11390
|
+
legendArea.height += legendLayout.bounds.height + gap;
|
|
11327
11391
|
break;
|
|
11328
11392
|
case "bottom":
|
|
11329
|
-
legendArea.height += legendLayout.bounds.height +
|
|
11393
|
+
legendArea.height += legendLayout.bounds.height + gap;
|
|
11330
11394
|
break;
|
|
11331
11395
|
case "right":
|
|
11332
11396
|
case "bottom-right":
|