@opendata-ai/openchart-engine 6.2.1 → 6.4.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 +203 -20
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/legend.test.ts +102 -0
- package/src/annotations/__tests__/compute.test.ts +107 -0
- package/src/annotations/compute.ts +87 -2
- package/src/charts/bar/__tests__/labels.test.ts +112 -0
- package/src/charts/bar/labels.ts +77 -4
- package/src/charts/line/labels.ts +6 -2
- package/src/charts/scatter/__tests__/compute.test.ts +121 -0
- package/src/charts/scatter/compute.ts +63 -12
- package/src/compile.ts +2 -0
- package/src/compiler/__tests__/validate.test.ts +34 -0
- package/src/compiler/validate.ts +34 -0
- package/src/layout/dimensions.ts +1 -0
- package/src/layout/scales.ts +4 -1
- package/src/legend/compute.ts +22 -2
- package/src/tooltips/__tests__/compute.test.ts +61 -0
- package/src/tooltips/compute.ts +14 -0
package/dist/index.js
CHANGED
|
@@ -209,6 +209,7 @@ function resolveTextAnnotation(annotation, scales, chartArea, isDark) {
|
|
|
209
209
|
};
|
|
210
210
|
return {
|
|
211
211
|
type: "text",
|
|
212
|
+
id: annotation.id,
|
|
212
213
|
label,
|
|
213
214
|
stroke: annotation.stroke,
|
|
214
215
|
fill: annotation.fill,
|
|
@@ -261,6 +262,7 @@ function resolveRangeAnnotation(annotation, scales, chartArea, isDark) {
|
|
|
261
262
|
const defaultOpacity = isDark ? 0.2 : DEFAULT_RANGE_OPACITY;
|
|
262
263
|
return {
|
|
263
264
|
type: "range",
|
|
265
|
+
id: annotation.id,
|
|
264
266
|
rect,
|
|
265
267
|
label,
|
|
266
268
|
fill: annotation.fill ?? DEFAULT_RANGE_FILL,
|
|
@@ -314,6 +316,7 @@ function resolveRefLineAnnotation(annotation, scales, chartArea, isDark) {
|
|
|
314
316
|
const defaultStroke = isDark ? DARK_REFLINE_STROKE : LIGHT_REFLINE_STROKE;
|
|
315
317
|
return {
|
|
316
318
|
type: "refline",
|
|
319
|
+
id: annotation.id,
|
|
317
320
|
line: { start, end },
|
|
318
321
|
label,
|
|
319
322
|
stroke: annotation.stroke ?? defaultStroke,
|
|
@@ -388,7 +391,7 @@ function nudgeAnnotationFromObstacles(annotation, originalAnnotation, scales, ch
|
|
|
388
391
|
if (stillCollides) continue;
|
|
389
392
|
const labelCenterX = candidateBounds.x + candidateBounds.width / 2;
|
|
390
393
|
const labelCenterY = candidateBounds.y + candidateBounds.height / 2;
|
|
391
|
-
const inBounds = labelCenterX >= chartArea.x && labelCenterX <= chartArea.x + chartArea.width +
|
|
394
|
+
const inBounds = labelCenterX >= chartArea.x && labelCenterX <= chartArea.x + chartArea.width + 10 && labelCenterY >= chartArea.y - fontSize && labelCenterY <= chartArea.y + chartArea.height + fontSize * 3;
|
|
392
395
|
if (inBounds) {
|
|
393
396
|
if (candidateLabel.connector && dx === 0 && dy !== 0) {
|
|
394
397
|
candidateLabel.connector = {
|
|
@@ -436,7 +439,7 @@ function resolveAnnotationCollisions(annotations, originalSpecs, scales, chartAr
|
|
|
436
439
|
if (stillCollides) continue;
|
|
437
440
|
const labelCenterX = candidateBounds.x + candidateBounds.width / 2;
|
|
438
441
|
const labelCenterY = candidateBounds.y + candidateBounds.height / 2;
|
|
439
|
-
const inBounds = labelCenterX >= chartArea.x && labelCenterX <= chartArea.x + chartArea.width +
|
|
442
|
+
const inBounds = labelCenterX >= chartArea.x && labelCenterX <= chartArea.x + chartArea.width + 10 && labelCenterY >= chartArea.y - fontSize && labelCenterY <= chartArea.y + chartArea.height + fontSize;
|
|
440
443
|
if (inBounds) {
|
|
441
444
|
let newConnector = annotation.label.connector;
|
|
442
445
|
if (newConnector) {
|
|
@@ -470,7 +473,54 @@ function resolveAnnotationCollisions(annotations, originalSpecs, scales, chartAr
|
|
|
470
473
|
placedBounds.push(estimateLabelBounds(annotation.label));
|
|
471
474
|
}
|
|
472
475
|
}
|
|
473
|
-
|
|
476
|
+
var CLAMP_MARGIN = 4;
|
|
477
|
+
function clampAnnotationsToBounds(annotations, svgWidth, svgHeight) {
|
|
478
|
+
for (const annotation of annotations) {
|
|
479
|
+
if (annotation.type !== "text" || !annotation.label) continue;
|
|
480
|
+
const bounds = estimateLabelBounds(annotation.label);
|
|
481
|
+
let dx = 0;
|
|
482
|
+
let dy = 0;
|
|
483
|
+
if (bounds.x + bounds.width > svgWidth - CLAMP_MARGIN) {
|
|
484
|
+
dx = svgWidth - CLAMP_MARGIN - (bounds.x + bounds.width);
|
|
485
|
+
}
|
|
486
|
+
if (bounds.x + dx < CLAMP_MARGIN) {
|
|
487
|
+
dx = CLAMP_MARGIN - bounds.x;
|
|
488
|
+
}
|
|
489
|
+
if (bounds.y < CLAMP_MARGIN) {
|
|
490
|
+
dy = CLAMP_MARGIN - bounds.y;
|
|
491
|
+
}
|
|
492
|
+
if (bounds.y + bounds.height + dy > svgHeight - CLAMP_MARGIN) {
|
|
493
|
+
dy = svgHeight - CLAMP_MARGIN - (bounds.y + bounds.height);
|
|
494
|
+
}
|
|
495
|
+
if (dx === 0 && dy === 0) continue;
|
|
496
|
+
const newX = annotation.label.x + dx;
|
|
497
|
+
const newY = annotation.label.y + dy;
|
|
498
|
+
let newConnector = annotation.label.connector;
|
|
499
|
+
if (newConnector) {
|
|
500
|
+
const fontSize = annotation.label.style.fontSize ?? DEFAULT_ANNOTATION_FONT_SIZE;
|
|
501
|
+
const fontWeight = annotation.label.style.fontWeight ?? DEFAULT_ANNOTATION_FONT_WEIGHT;
|
|
502
|
+
const connStyle = newConnector.style === "curve" ? "curve" : "straight";
|
|
503
|
+
const newFrom = computeConnectorOrigin(
|
|
504
|
+
newX,
|
|
505
|
+
newY,
|
|
506
|
+
annotation.label.text,
|
|
507
|
+
fontSize,
|
|
508
|
+
fontWeight,
|
|
509
|
+
newConnector.to.x,
|
|
510
|
+
newConnector.to.y,
|
|
511
|
+
connStyle
|
|
512
|
+
);
|
|
513
|
+
newConnector = { ...newConnector, from: newFrom };
|
|
514
|
+
}
|
|
515
|
+
annotation.label = {
|
|
516
|
+
...annotation.label,
|
|
517
|
+
x: newX,
|
|
518
|
+
y: newY,
|
|
519
|
+
connector: newConnector
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
function computeAnnotations(spec, scales, chartArea, strategy, isDark = false, obstacles = [], svgDimensions) {
|
|
474
524
|
if (strategy.annotationPosition === "tooltip-only") {
|
|
475
525
|
return [];
|
|
476
526
|
}
|
|
@@ -496,6 +546,9 @@ function computeAnnotations(spec, scales, chartArea, strategy, isDark = false, o
|
|
|
496
546
|
}
|
|
497
547
|
}
|
|
498
548
|
resolveAnnotationCollisions(annotations, spec.annotations, scales, chartArea);
|
|
549
|
+
if (svgDimensions) {
|
|
550
|
+
clampAnnotationsToBounds(annotations, svgDimensions.width, svgDimensions.height);
|
|
551
|
+
}
|
|
499
552
|
annotations.sort((a, b) => (a.zIndex ?? 0) - (b.zIndex ?? 0));
|
|
500
553
|
return annotations;
|
|
501
554
|
}
|
|
@@ -772,6 +825,24 @@ import {
|
|
|
772
825
|
estimateTextWidth as estimateTextWidth2,
|
|
773
826
|
resolveCollisions
|
|
774
827
|
} from "@opendata-ai/openchart-core";
|
|
828
|
+
var SUFFIX_MULTIPLIERS = {
|
|
829
|
+
K: 1e3,
|
|
830
|
+
M: 1e6,
|
|
831
|
+
B: 1e9,
|
|
832
|
+
T: 1e12
|
|
833
|
+
};
|
|
834
|
+
function parseDisplayNumber(raw) {
|
|
835
|
+
const trimmed = raw.trim();
|
|
836
|
+
if (!trimmed) return NaN;
|
|
837
|
+
const last = trimmed[trimmed.length - 1].toUpperCase();
|
|
838
|
+
const multiplier = SUFFIX_MULTIPLIERS[last];
|
|
839
|
+
if (multiplier) {
|
|
840
|
+
const numPart = trimmed.slice(0, -1).replace(/,/g, "");
|
|
841
|
+
const n = Number(numPart);
|
|
842
|
+
return Number.isNaN(n) ? NaN : n * multiplier;
|
|
843
|
+
}
|
|
844
|
+
return Number(trimmed.replace(/,/g, ""));
|
|
845
|
+
}
|
|
775
846
|
var LABEL_FONT_SIZE = 11;
|
|
776
847
|
var LABEL_FONT_WEIGHT = 600;
|
|
777
848
|
var LABEL_PADDING = 6;
|
|
@@ -780,6 +851,7 @@ function computeBarLabels(marks, _chartArea, density = "auto", labelFormat) {
|
|
|
780
851
|
if (density === "none") return [];
|
|
781
852
|
const targetMarks = density === "endpoints" && marks.length > 1 ? [marks[0], marks[marks.length - 1]] : marks;
|
|
782
853
|
const candidates = [];
|
|
854
|
+
const fitsInSegment = [];
|
|
783
855
|
const formatter = buildD3Formatter(labelFormat);
|
|
784
856
|
for (const mark of targetMarks) {
|
|
785
857
|
const ariaLabel = mark.aria.label;
|
|
@@ -788,7 +860,7 @@ function computeBarLabels(marks, _chartArea, density = "auto", labelFormat) {
|
|
|
788
860
|
if (!rawValue) continue;
|
|
789
861
|
let valuePart = rawValue;
|
|
790
862
|
if (formatter) {
|
|
791
|
-
const num =
|
|
863
|
+
const num = parseDisplayNumber(rawValue);
|
|
792
864
|
if (!Number.isNaN(num)) valuePart = formatter(num);
|
|
793
865
|
}
|
|
794
866
|
const textWidth = estimateTextWidth2(valuePart, LABEL_FONT_SIZE, LABEL_FONT_WEIGHT);
|
|
@@ -812,6 +884,8 @@ function computeBarLabels(marks, _chartArea, density = "auto", labelFormat) {
|
|
|
812
884
|
textAnchor = "start";
|
|
813
885
|
}
|
|
814
886
|
const anchorY = mark.y + mark.height / 2;
|
|
887
|
+
const fits = !(isStacked && textWidth > mark.width - 2 * LABEL_PADDING);
|
|
888
|
+
fitsInSegment.push(fits);
|
|
815
889
|
candidates.push({
|
|
816
890
|
text: valuePart,
|
|
817
891
|
anchorX,
|
|
@@ -832,15 +906,40 @@ function computeBarLabels(marks, _chartArea, density = "auto", labelFormat) {
|
|
|
832
906
|
}
|
|
833
907
|
if (candidates.length === 0) return [];
|
|
834
908
|
if (density === "all") {
|
|
835
|
-
return candidates.map((c) => ({
|
|
909
|
+
return candidates.map((c, i) => ({
|
|
836
910
|
text: c.text,
|
|
837
911
|
x: c.anchorX,
|
|
838
912
|
y: c.anchorY,
|
|
839
913
|
style: c.style,
|
|
840
|
-
visible:
|
|
914
|
+
visible: fitsInSegment[i] !== false
|
|
841
915
|
}));
|
|
842
916
|
}
|
|
843
|
-
|
|
917
|
+
const fittingCandidates = [];
|
|
918
|
+
const unfittingIndices = /* @__PURE__ */ new Set();
|
|
919
|
+
for (let i = 0; i < candidates.length; i++) {
|
|
920
|
+
if (fitsInSegment[i] === false) {
|
|
921
|
+
unfittingIndices.add(i);
|
|
922
|
+
} else {
|
|
923
|
+
fittingCandidates.push(candidates[i]);
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
const resolved = resolveCollisions(fittingCandidates);
|
|
927
|
+
const results = [];
|
|
928
|
+
let resolvedIdx = 0;
|
|
929
|
+
for (let i = 0; i < candidates.length; i++) {
|
|
930
|
+
if (unfittingIndices.has(i)) {
|
|
931
|
+
results.push({
|
|
932
|
+
text: candidates[i].text,
|
|
933
|
+
x: candidates[i].anchorX,
|
|
934
|
+
y: candidates[i].anchorY,
|
|
935
|
+
style: candidates[i].style,
|
|
936
|
+
visible: false
|
|
937
|
+
});
|
|
938
|
+
} else {
|
|
939
|
+
results.push(resolved[resolvedIdx++]);
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
return results;
|
|
844
943
|
}
|
|
845
944
|
|
|
846
945
|
// src/charts/bar/index.ts
|
|
@@ -2487,7 +2586,11 @@ function _getMidValue(rows, field) {
|
|
|
2487
2586
|
}
|
|
2488
2587
|
|
|
2489
2588
|
// src/charts/line/labels.ts
|
|
2490
|
-
import {
|
|
2589
|
+
import {
|
|
2590
|
+
EXTENDED_OFFSET_STRATEGIES,
|
|
2591
|
+
estimateTextWidth as estimateTextWidth5,
|
|
2592
|
+
resolveCollisions as resolveCollisions4
|
|
2593
|
+
} from "@opendata-ai/openchart-core";
|
|
2491
2594
|
var LABEL_FONT_SIZE4 = 11;
|
|
2492
2595
|
var LABEL_FONT_WEIGHT4 = 600;
|
|
2493
2596
|
var LABEL_OFFSET_X2 = 6;
|
|
@@ -2541,7 +2644,7 @@ function computeLineLabels(marks, strategy, density = "auto", labelOffsets) {
|
|
|
2541
2644
|
}
|
|
2542
2645
|
return result;
|
|
2543
2646
|
}
|
|
2544
|
-
const resolved = resolveCollisions4(candidates);
|
|
2647
|
+
const resolved = resolveCollisions4(candidates, EXTENDED_OFFSET_STRATEGIES);
|
|
2545
2648
|
for (let i = 0; i < resolved.length; i++) {
|
|
2546
2649
|
const seriesKey = seriesOrder[i];
|
|
2547
2650
|
const label = resolved[i];
|
|
@@ -5494,6 +5597,30 @@ function sequential() {
|
|
|
5494
5597
|
var DEFAULT_POINT_RADIUS2 = 5;
|
|
5495
5598
|
var MIN_BUBBLE_RADIUS = 3;
|
|
5496
5599
|
var MAX_BUBBLE_RADIUS = 30;
|
|
5600
|
+
function resolvePosition2(value, channelType, scale) {
|
|
5601
|
+
switch (channelType) {
|
|
5602
|
+
case "nominal":
|
|
5603
|
+
case "ordinal": {
|
|
5604
|
+
const s = String(value);
|
|
5605
|
+
if ("bandwidth" in scale && typeof scale.bandwidth === "function") {
|
|
5606
|
+
const bw = scale.bandwidth();
|
|
5607
|
+
const pos = scale(s);
|
|
5608
|
+
if (pos === void 0) return void 0;
|
|
5609
|
+
return bw > 0 ? pos + bw / 2 : pos;
|
|
5610
|
+
}
|
|
5611
|
+
return scale(s);
|
|
5612
|
+
}
|
|
5613
|
+
case "temporal": {
|
|
5614
|
+
const px = scale(new Date(value));
|
|
5615
|
+
return Number.isNaN(px) ? void 0 : px;
|
|
5616
|
+
}
|
|
5617
|
+
default: {
|
|
5618
|
+
const num = Number(value);
|
|
5619
|
+
if (!Number.isFinite(num)) return void 0;
|
|
5620
|
+
return scale(num);
|
|
5621
|
+
}
|
|
5622
|
+
}
|
|
5623
|
+
}
|
|
5497
5624
|
function computeScatterMarks(spec, scales, _chartArea, _strategy) {
|
|
5498
5625
|
const encoding = spec.encoding;
|
|
5499
5626
|
const xChannel = encoding.x;
|
|
@@ -5503,6 +5630,8 @@ function computeScatterMarks(spec, scales, _chartArea, _strategy) {
|
|
|
5503
5630
|
}
|
|
5504
5631
|
const xScale = scales.x.scale;
|
|
5505
5632
|
const yScale = scales.y.scale;
|
|
5633
|
+
const xType = xChannel.type;
|
|
5634
|
+
const yType = yChannel.type;
|
|
5506
5635
|
const colorEnc = encoding.color && "field" in encoding.color ? encoding.color : void 0;
|
|
5507
5636
|
const isSequentialColor = colorEnc?.type === "quantitative";
|
|
5508
5637
|
const colorField = colorEnc?.field;
|
|
@@ -5516,11 +5645,11 @@ function computeScatterMarks(spec, scales, _chartArea, _strategy) {
|
|
|
5516
5645
|
}
|
|
5517
5646
|
const marks = [];
|
|
5518
5647
|
for (const row of spec.data) {
|
|
5519
|
-
const
|
|
5520
|
-
const
|
|
5521
|
-
|
|
5522
|
-
const
|
|
5523
|
-
|
|
5648
|
+
const rawX = row[xChannel.field];
|
|
5649
|
+
const rawY = row[yChannel.field];
|
|
5650
|
+
const cx = resolvePosition2(rawX, xType, xScale);
|
|
5651
|
+
const cy = resolvePosition2(rawY, yType, yScale);
|
|
5652
|
+
if (cx === void 0 || cy === void 0) continue;
|
|
5524
5653
|
const category = colorField && !isSequentialColor ? String(row[colorField] ?? "") : void 0;
|
|
5525
5654
|
let color2;
|
|
5526
5655
|
if (isSequentialColor && colorField) {
|
|
@@ -5536,7 +5665,7 @@ function computeScatterMarks(spec, scales, _chartArea, _strategy) {
|
|
|
5536
5665
|
radius = sizeScale(sizeVal);
|
|
5537
5666
|
}
|
|
5538
5667
|
}
|
|
5539
|
-
const labelParts = [`${xChannel.field}=${
|
|
5668
|
+
const labelParts = [`${xChannel.field}=${rawX}`, `${yChannel.field}=${rawY}`];
|
|
5540
5669
|
if (category) labelParts.push(`${colorField}=${category}`);
|
|
5541
5670
|
if (sizeField && row[sizeField] != null) {
|
|
5542
5671
|
labelParts.push(`${sizeField}=${row[sizeField]}`);
|
|
@@ -6026,6 +6155,38 @@ function validateChartSpec(spec, errors) {
|
|
|
6026
6155
|
}
|
|
6027
6156
|
for (const [channel, channelSpec] of Object.entries(encoding)) {
|
|
6028
6157
|
if (!channelSpec || typeof channelSpec !== "object") continue;
|
|
6158
|
+
if (channel === "tooltip" && Array.isArray(channelSpec)) {
|
|
6159
|
+
for (let i = 0; i < channelSpec.length; i++) {
|
|
6160
|
+
const elem = channelSpec[i];
|
|
6161
|
+
if (!elem || typeof elem !== "object") continue;
|
|
6162
|
+
if (!elem.field || typeof elem.field !== "string") {
|
|
6163
|
+
errors.push({
|
|
6164
|
+
message: `Spec error: encoding.tooltip[${i}] must have a "field" string`,
|
|
6165
|
+
path: `encoding.tooltip[${i}].field`,
|
|
6166
|
+
code: "MISSING_FIELD",
|
|
6167
|
+
suggestion: `Add a field name from your data columns: ${availableColumns}`
|
|
6168
|
+
});
|
|
6169
|
+
continue;
|
|
6170
|
+
}
|
|
6171
|
+
if (!dataColumns.has(elem.field) && !transformFields.has(elem.field)) {
|
|
6172
|
+
errors.push({
|
|
6173
|
+
message: `Spec error: encoding.tooltip[${i}].field "${elem.field}" does not exist in data. Available columns: ${availableColumns}`,
|
|
6174
|
+
path: `encoding.tooltip[${i}].field`,
|
|
6175
|
+
code: "DATA_FIELD_MISSING",
|
|
6176
|
+
suggestion: `Use one of the available data columns: ${availableColumns}`
|
|
6177
|
+
});
|
|
6178
|
+
}
|
|
6179
|
+
if (elem.type && !VALID_FIELD_TYPES.has(elem.type)) {
|
|
6180
|
+
errors.push({
|
|
6181
|
+
message: `Spec error: encoding.tooltip[${i}].type "${elem.type}" is not valid. Must be one of: ${[...VALID_FIELD_TYPES].join(", ")}`,
|
|
6182
|
+
path: `encoding.tooltip[${i}].type`,
|
|
6183
|
+
code: "INVALID_VALUE",
|
|
6184
|
+
suggestion: `Use one of: ${[...VALID_FIELD_TYPES].join(", ")}`
|
|
6185
|
+
});
|
|
6186
|
+
}
|
|
6187
|
+
}
|
|
6188
|
+
continue;
|
|
6189
|
+
}
|
|
6029
6190
|
const channelObj = channelSpec;
|
|
6030
6191
|
const channelRule = rules[channel];
|
|
6031
6192
|
if ("condition" in channelObj) continue;
|
|
@@ -7276,7 +7437,7 @@ function computeDimensions(spec, options, legendLayout, theme, strategy) {
|
|
|
7276
7437
|
}
|
|
7277
7438
|
}
|
|
7278
7439
|
if (encoding.y && !isRadial) {
|
|
7279
|
-
if (spec.markType === "bar" || spec.markType === "circle" || encoding.y.type === "nominal" || encoding.y.type === "ordinal") {
|
|
7440
|
+
if (spec.markType === "bar" || spec.markType === "circle" || spec.markType === "lollipop" || encoding.y.type === "nominal" || encoding.y.type === "ordinal") {
|
|
7280
7441
|
const yField = encoding.y.field;
|
|
7281
7442
|
let maxLabelWidth = 0;
|
|
7282
7443
|
for (const row of spec.data) {
|
|
@@ -7658,7 +7819,7 @@ function buildPositionalScale(channel, data, rangeStart, rangeEnd, chartType, ax
|
|
|
7658
7819
|
return buildLinearScale(channel, data, rangeStart, rangeEnd);
|
|
7659
7820
|
case "nominal":
|
|
7660
7821
|
case "ordinal":
|
|
7661
|
-
if (chartType === "bar" || chartType === "circle" && axis === "y") {
|
|
7822
|
+
if (chartType === "bar" || (chartType === "circle" || chartType === "lollipop") && axis === "y") {
|
|
7662
7823
|
return buildBandScale(channel, data, rangeStart, rangeEnd);
|
|
7663
7824
|
}
|
|
7664
7825
|
return buildPointScale(channel, data, rangeStart, rangeEnd);
|
|
@@ -7779,6 +7940,7 @@ function swatchShapeForType(markType) {
|
|
|
7779
7940
|
return "line";
|
|
7780
7941
|
case "point":
|
|
7781
7942
|
case "circle":
|
|
7943
|
+
case "lollipop":
|
|
7782
7944
|
return "circle";
|
|
7783
7945
|
default:
|
|
7784
7946
|
return "square";
|
|
@@ -7881,10 +8043,11 @@ function computeLegend(spec, strategy, theme, chartArea) {
|
|
|
7881
8043
|
const entryHeight = Math.max(SWATCH_SIZE2, labelStyle.fontSize * labelStyle.lineHeight);
|
|
7882
8044
|
const maxHeightRatio = strategy.legendMaxHeight > 0 ? strategy.legendMaxHeight : RIGHT_LEGEND_MAX_HEIGHT_RATIO;
|
|
7883
8045
|
const maxLegendHeight = chartArea.height * maxHeightRatio;
|
|
7884
|
-
const
|
|
8046
|
+
const maxFromSpace = Math.max(
|
|
7885
8047
|
1,
|
|
7886
8048
|
Math.floor((maxLegendHeight - LEGEND_PADDING * 2) / (entryHeight + 4))
|
|
7887
8049
|
);
|
|
8050
|
+
const maxEntries = spec.legend?.symbolLimit != null ? Math.min(Math.max(1, spec.legend.symbolLimit), maxFromSpace) : maxFromSpace;
|
|
7888
8051
|
if (entries.length > maxEntries) {
|
|
7889
8052
|
entries = truncateEntries(entries, maxEntries);
|
|
7890
8053
|
}
|
|
@@ -7909,7 +8072,14 @@ function computeLegend(spec, strategy, theme, chartArea) {
|
|
|
7909
8072
|
};
|
|
7910
8073
|
}
|
|
7911
8074
|
const availableWidth = chartArea.width - LEGEND_PADDING * 2 - BRAND_RESERVE_WIDTH;
|
|
7912
|
-
|
|
8075
|
+
if (spec.legend?.symbolLimit != null) {
|
|
8076
|
+
const limit = Math.max(1, spec.legend.symbolLimit);
|
|
8077
|
+
if (limit < entries.length) {
|
|
8078
|
+
entries = truncateEntries(entries, limit);
|
|
8079
|
+
}
|
|
8080
|
+
}
|
|
8081
|
+
const maxRows = spec.legend?.columns != null ? Math.ceil(entries.length / spec.legend.columns) : TOP_LEGEND_MAX_ROWS;
|
|
8082
|
+
const maxFit = entriesThatFit(entries, availableWidth, maxRows, labelStyle);
|
|
7913
8083
|
if (maxFit < entries.length) {
|
|
7914
8084
|
entries = truncateEntries(entries, maxFit);
|
|
7915
8085
|
}
|
|
@@ -8602,7 +8772,17 @@ function formatValue(value, fieldType, format2) {
|
|
|
8602
8772
|
}
|
|
8603
8773
|
return String(value);
|
|
8604
8774
|
}
|
|
8775
|
+
function buildExplicitTooltipFields(row, channels) {
|
|
8776
|
+
return channels.map((ch) => ({
|
|
8777
|
+
label: ch.axis?.label ?? ch.field,
|
|
8778
|
+
value: formatValue(row[ch.field], ch.type, ch.axis?.format)
|
|
8779
|
+
}));
|
|
8780
|
+
}
|
|
8605
8781
|
function buildFields(row, encoding, color2) {
|
|
8782
|
+
if (encoding.tooltip) {
|
|
8783
|
+
const channels = Array.isArray(encoding.tooltip) ? encoding.tooltip : [encoding.tooltip];
|
|
8784
|
+
return buildExplicitTooltipFields(row, channels);
|
|
8785
|
+
}
|
|
8606
8786
|
const fields = [];
|
|
8607
8787
|
if (encoding.y) {
|
|
8608
8788
|
fields.push({
|
|
@@ -8922,6 +9102,8 @@ var builtinRenderers = {
|
|
|
8922
9102
|
// old 'donut'
|
|
8923
9103
|
circle: dotRenderer,
|
|
8924
9104
|
// old 'dot'
|
|
9105
|
+
lollipop: dotRenderer,
|
|
9106
|
+
// semantic alias for dot/circle
|
|
8925
9107
|
text: textRenderer,
|
|
8926
9108
|
rule: ruleRenderer,
|
|
8927
9109
|
tick: tickRenderer,
|
|
@@ -9157,7 +9339,8 @@ function compileChart(spec, options) {
|
|
|
9157
9339
|
chartArea,
|
|
9158
9340
|
strategy,
|
|
9159
9341
|
theme.isDark,
|
|
9160
|
-
obstacles
|
|
9342
|
+
obstacles,
|
|
9343
|
+
{ width: dims.total.width, height: dims.total.height }
|
|
9161
9344
|
);
|
|
9162
9345
|
const tooltipDescriptors = computeTooltipDescriptors(chartSpec, marks);
|
|
9163
9346
|
const altText = generateAltText(
|