@opendata-ai/openchart-engine 6.2.0 → 6.3.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 +215 -22
- 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 +84 -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/axes.ts +2 -1
- package/src/layout/dimensions.ts +19 -1
- 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
|
@@ -388,7 +388,7 @@ function nudgeAnnotationFromObstacles(annotation, originalAnnotation, scales, ch
|
|
|
388
388
|
if (stillCollides) continue;
|
|
389
389
|
const labelCenterX = candidateBounds.x + candidateBounds.width / 2;
|
|
390
390
|
const labelCenterY = candidateBounds.y + candidateBounds.height / 2;
|
|
391
|
-
const inBounds = labelCenterX >= chartArea.x && labelCenterX <= chartArea.x + chartArea.width +
|
|
391
|
+
const inBounds = labelCenterX >= chartArea.x && labelCenterX <= chartArea.x + chartArea.width + 10 && labelCenterY >= chartArea.y - fontSize && labelCenterY <= chartArea.y + chartArea.height + fontSize * 3;
|
|
392
392
|
if (inBounds) {
|
|
393
393
|
if (candidateLabel.connector && dx === 0 && dy !== 0) {
|
|
394
394
|
candidateLabel.connector = {
|
|
@@ -436,7 +436,7 @@ function resolveAnnotationCollisions(annotations, originalSpecs, scales, chartAr
|
|
|
436
436
|
if (stillCollides) continue;
|
|
437
437
|
const labelCenterX = candidateBounds.x + candidateBounds.width / 2;
|
|
438
438
|
const labelCenterY = candidateBounds.y + candidateBounds.height / 2;
|
|
439
|
-
const inBounds = labelCenterX >= chartArea.x && labelCenterX <= chartArea.x + chartArea.width +
|
|
439
|
+
const inBounds = labelCenterX >= chartArea.x && labelCenterX <= chartArea.x + chartArea.width + 10 && labelCenterY >= chartArea.y - fontSize && labelCenterY <= chartArea.y + chartArea.height + fontSize;
|
|
440
440
|
if (inBounds) {
|
|
441
441
|
let newConnector = annotation.label.connector;
|
|
442
442
|
if (newConnector) {
|
|
@@ -470,7 +470,54 @@ function resolveAnnotationCollisions(annotations, originalSpecs, scales, chartAr
|
|
|
470
470
|
placedBounds.push(estimateLabelBounds(annotation.label));
|
|
471
471
|
}
|
|
472
472
|
}
|
|
473
|
-
|
|
473
|
+
var CLAMP_MARGIN = 4;
|
|
474
|
+
function clampAnnotationsToBounds(annotations, svgWidth, svgHeight) {
|
|
475
|
+
for (const annotation of annotations) {
|
|
476
|
+
if (annotation.type !== "text" || !annotation.label) continue;
|
|
477
|
+
const bounds = estimateLabelBounds(annotation.label);
|
|
478
|
+
let dx = 0;
|
|
479
|
+
let dy = 0;
|
|
480
|
+
if (bounds.x + bounds.width > svgWidth - CLAMP_MARGIN) {
|
|
481
|
+
dx = svgWidth - CLAMP_MARGIN - (bounds.x + bounds.width);
|
|
482
|
+
}
|
|
483
|
+
if (bounds.x + dx < CLAMP_MARGIN) {
|
|
484
|
+
dx = CLAMP_MARGIN - bounds.x;
|
|
485
|
+
}
|
|
486
|
+
if (bounds.y < CLAMP_MARGIN) {
|
|
487
|
+
dy = CLAMP_MARGIN - bounds.y;
|
|
488
|
+
}
|
|
489
|
+
if (bounds.y + bounds.height + dy > svgHeight - CLAMP_MARGIN) {
|
|
490
|
+
dy = svgHeight - CLAMP_MARGIN - (bounds.y + bounds.height);
|
|
491
|
+
}
|
|
492
|
+
if (dx === 0 && dy === 0) continue;
|
|
493
|
+
const newX = annotation.label.x + dx;
|
|
494
|
+
const newY = annotation.label.y + dy;
|
|
495
|
+
let newConnector = annotation.label.connector;
|
|
496
|
+
if (newConnector) {
|
|
497
|
+
const fontSize = annotation.label.style.fontSize ?? DEFAULT_ANNOTATION_FONT_SIZE;
|
|
498
|
+
const fontWeight = annotation.label.style.fontWeight ?? DEFAULT_ANNOTATION_FONT_WEIGHT;
|
|
499
|
+
const connStyle = newConnector.style === "curve" ? "curve" : "straight";
|
|
500
|
+
const newFrom = computeConnectorOrigin(
|
|
501
|
+
newX,
|
|
502
|
+
newY,
|
|
503
|
+
annotation.label.text,
|
|
504
|
+
fontSize,
|
|
505
|
+
fontWeight,
|
|
506
|
+
newConnector.to.x,
|
|
507
|
+
newConnector.to.y,
|
|
508
|
+
connStyle
|
|
509
|
+
);
|
|
510
|
+
newConnector = { ...newConnector, from: newFrom };
|
|
511
|
+
}
|
|
512
|
+
annotation.label = {
|
|
513
|
+
...annotation.label,
|
|
514
|
+
x: newX,
|
|
515
|
+
y: newY,
|
|
516
|
+
connector: newConnector
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
function computeAnnotations(spec, scales, chartArea, strategy, isDark = false, obstacles = [], svgDimensions) {
|
|
474
521
|
if (strategy.annotationPosition === "tooltip-only") {
|
|
475
522
|
return [];
|
|
476
523
|
}
|
|
@@ -496,6 +543,9 @@ function computeAnnotations(spec, scales, chartArea, strategy, isDark = false, o
|
|
|
496
543
|
}
|
|
497
544
|
}
|
|
498
545
|
resolveAnnotationCollisions(annotations, spec.annotations, scales, chartArea);
|
|
546
|
+
if (svgDimensions) {
|
|
547
|
+
clampAnnotationsToBounds(annotations, svgDimensions.width, svgDimensions.height);
|
|
548
|
+
}
|
|
499
549
|
annotations.sort((a, b) => (a.zIndex ?? 0) - (b.zIndex ?? 0));
|
|
500
550
|
return annotations;
|
|
501
551
|
}
|
|
@@ -772,6 +822,24 @@ import {
|
|
|
772
822
|
estimateTextWidth as estimateTextWidth2,
|
|
773
823
|
resolveCollisions
|
|
774
824
|
} from "@opendata-ai/openchart-core";
|
|
825
|
+
var SUFFIX_MULTIPLIERS = {
|
|
826
|
+
K: 1e3,
|
|
827
|
+
M: 1e6,
|
|
828
|
+
B: 1e9,
|
|
829
|
+
T: 1e12
|
|
830
|
+
};
|
|
831
|
+
function parseDisplayNumber(raw) {
|
|
832
|
+
const trimmed = raw.trim();
|
|
833
|
+
if (!trimmed) return NaN;
|
|
834
|
+
const last = trimmed[trimmed.length - 1].toUpperCase();
|
|
835
|
+
const multiplier = SUFFIX_MULTIPLIERS[last];
|
|
836
|
+
if (multiplier) {
|
|
837
|
+
const numPart = trimmed.slice(0, -1).replace(/,/g, "");
|
|
838
|
+
const n = Number(numPart);
|
|
839
|
+
return Number.isNaN(n) ? NaN : n * multiplier;
|
|
840
|
+
}
|
|
841
|
+
return Number(trimmed.replace(/,/g, ""));
|
|
842
|
+
}
|
|
775
843
|
var LABEL_FONT_SIZE = 11;
|
|
776
844
|
var LABEL_FONT_WEIGHT = 600;
|
|
777
845
|
var LABEL_PADDING = 6;
|
|
@@ -780,6 +848,7 @@ function computeBarLabels(marks, _chartArea, density = "auto", labelFormat) {
|
|
|
780
848
|
if (density === "none") return [];
|
|
781
849
|
const targetMarks = density === "endpoints" && marks.length > 1 ? [marks[0], marks[marks.length - 1]] : marks;
|
|
782
850
|
const candidates = [];
|
|
851
|
+
const fitsInSegment = [];
|
|
783
852
|
const formatter = buildD3Formatter(labelFormat);
|
|
784
853
|
for (const mark of targetMarks) {
|
|
785
854
|
const ariaLabel = mark.aria.label;
|
|
@@ -788,7 +857,7 @@ function computeBarLabels(marks, _chartArea, density = "auto", labelFormat) {
|
|
|
788
857
|
if (!rawValue) continue;
|
|
789
858
|
let valuePart = rawValue;
|
|
790
859
|
if (formatter) {
|
|
791
|
-
const num =
|
|
860
|
+
const num = parseDisplayNumber(rawValue);
|
|
792
861
|
if (!Number.isNaN(num)) valuePart = formatter(num);
|
|
793
862
|
}
|
|
794
863
|
const textWidth = estimateTextWidth2(valuePart, LABEL_FONT_SIZE, LABEL_FONT_WEIGHT);
|
|
@@ -812,6 +881,8 @@ function computeBarLabels(marks, _chartArea, density = "auto", labelFormat) {
|
|
|
812
881
|
textAnchor = "start";
|
|
813
882
|
}
|
|
814
883
|
const anchorY = mark.y + mark.height / 2;
|
|
884
|
+
const fits = !(isStacked && textWidth > mark.width - 2 * LABEL_PADDING);
|
|
885
|
+
fitsInSegment.push(fits);
|
|
815
886
|
candidates.push({
|
|
816
887
|
text: valuePart,
|
|
817
888
|
anchorX,
|
|
@@ -832,15 +903,40 @@ function computeBarLabels(marks, _chartArea, density = "auto", labelFormat) {
|
|
|
832
903
|
}
|
|
833
904
|
if (candidates.length === 0) return [];
|
|
834
905
|
if (density === "all") {
|
|
835
|
-
return candidates.map((c) => ({
|
|
906
|
+
return candidates.map((c, i) => ({
|
|
836
907
|
text: c.text,
|
|
837
908
|
x: c.anchorX,
|
|
838
909
|
y: c.anchorY,
|
|
839
910
|
style: c.style,
|
|
840
|
-
visible:
|
|
911
|
+
visible: fitsInSegment[i] !== false
|
|
841
912
|
}));
|
|
842
913
|
}
|
|
843
|
-
|
|
914
|
+
const fittingCandidates = [];
|
|
915
|
+
const unfittingIndices = /* @__PURE__ */ new Set();
|
|
916
|
+
for (let i = 0; i < candidates.length; i++) {
|
|
917
|
+
if (fitsInSegment[i] === false) {
|
|
918
|
+
unfittingIndices.add(i);
|
|
919
|
+
} else {
|
|
920
|
+
fittingCandidates.push(candidates[i]);
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
const resolved = resolveCollisions(fittingCandidates);
|
|
924
|
+
const results = [];
|
|
925
|
+
let resolvedIdx = 0;
|
|
926
|
+
for (let i = 0; i < candidates.length; i++) {
|
|
927
|
+
if (unfittingIndices.has(i)) {
|
|
928
|
+
results.push({
|
|
929
|
+
text: candidates[i].text,
|
|
930
|
+
x: candidates[i].anchorX,
|
|
931
|
+
y: candidates[i].anchorY,
|
|
932
|
+
style: candidates[i].style,
|
|
933
|
+
visible: false
|
|
934
|
+
});
|
|
935
|
+
} else {
|
|
936
|
+
results.push(resolved[resolvedIdx++]);
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
return results;
|
|
844
940
|
}
|
|
845
941
|
|
|
846
942
|
// src/charts/bar/index.ts
|
|
@@ -2487,7 +2583,11 @@ function _getMidValue(rows, field) {
|
|
|
2487
2583
|
}
|
|
2488
2584
|
|
|
2489
2585
|
// src/charts/line/labels.ts
|
|
2490
|
-
import {
|
|
2586
|
+
import {
|
|
2587
|
+
EXTENDED_OFFSET_STRATEGIES,
|
|
2588
|
+
estimateTextWidth as estimateTextWidth5,
|
|
2589
|
+
resolveCollisions as resolveCollisions4
|
|
2590
|
+
} from "@opendata-ai/openchart-core";
|
|
2491
2591
|
var LABEL_FONT_SIZE4 = 11;
|
|
2492
2592
|
var LABEL_FONT_WEIGHT4 = 600;
|
|
2493
2593
|
var LABEL_OFFSET_X2 = 6;
|
|
@@ -2541,7 +2641,7 @@ function computeLineLabels(marks, strategy, density = "auto", labelOffsets) {
|
|
|
2541
2641
|
}
|
|
2542
2642
|
return result;
|
|
2543
2643
|
}
|
|
2544
|
-
const resolved = resolveCollisions4(candidates);
|
|
2644
|
+
const resolved = resolveCollisions4(candidates, EXTENDED_OFFSET_STRATEGIES);
|
|
2545
2645
|
for (let i = 0; i < resolved.length; i++) {
|
|
2546
2646
|
const seriesKey = seriesOrder[i];
|
|
2547
2647
|
const label = resolved[i];
|
|
@@ -5494,6 +5594,30 @@ function sequential() {
|
|
|
5494
5594
|
var DEFAULT_POINT_RADIUS2 = 5;
|
|
5495
5595
|
var MIN_BUBBLE_RADIUS = 3;
|
|
5496
5596
|
var MAX_BUBBLE_RADIUS = 30;
|
|
5597
|
+
function resolvePosition2(value, channelType, scale) {
|
|
5598
|
+
switch (channelType) {
|
|
5599
|
+
case "nominal":
|
|
5600
|
+
case "ordinal": {
|
|
5601
|
+
const s = String(value);
|
|
5602
|
+
if ("bandwidth" in scale && typeof scale.bandwidth === "function") {
|
|
5603
|
+
const bw = scale.bandwidth();
|
|
5604
|
+
const pos = scale(s);
|
|
5605
|
+
if (pos === void 0) return void 0;
|
|
5606
|
+
return bw > 0 ? pos + bw / 2 : pos;
|
|
5607
|
+
}
|
|
5608
|
+
return scale(s);
|
|
5609
|
+
}
|
|
5610
|
+
case "temporal": {
|
|
5611
|
+
const px = scale(new Date(value));
|
|
5612
|
+
return Number.isNaN(px) ? void 0 : px;
|
|
5613
|
+
}
|
|
5614
|
+
default: {
|
|
5615
|
+
const num = Number(value);
|
|
5616
|
+
if (!Number.isFinite(num)) return void 0;
|
|
5617
|
+
return scale(num);
|
|
5618
|
+
}
|
|
5619
|
+
}
|
|
5620
|
+
}
|
|
5497
5621
|
function computeScatterMarks(spec, scales, _chartArea, _strategy) {
|
|
5498
5622
|
const encoding = spec.encoding;
|
|
5499
5623
|
const xChannel = encoding.x;
|
|
@@ -5503,6 +5627,8 @@ function computeScatterMarks(spec, scales, _chartArea, _strategy) {
|
|
|
5503
5627
|
}
|
|
5504
5628
|
const xScale = scales.x.scale;
|
|
5505
5629
|
const yScale = scales.y.scale;
|
|
5630
|
+
const xType = xChannel.type;
|
|
5631
|
+
const yType = yChannel.type;
|
|
5506
5632
|
const colorEnc = encoding.color && "field" in encoding.color ? encoding.color : void 0;
|
|
5507
5633
|
const isSequentialColor = colorEnc?.type === "quantitative";
|
|
5508
5634
|
const colorField = colorEnc?.field;
|
|
@@ -5516,11 +5642,11 @@ function computeScatterMarks(spec, scales, _chartArea, _strategy) {
|
|
|
5516
5642
|
}
|
|
5517
5643
|
const marks = [];
|
|
5518
5644
|
for (const row of spec.data) {
|
|
5519
|
-
const
|
|
5520
|
-
const
|
|
5521
|
-
|
|
5522
|
-
const
|
|
5523
|
-
|
|
5645
|
+
const rawX = row[xChannel.field];
|
|
5646
|
+
const rawY = row[yChannel.field];
|
|
5647
|
+
const cx = resolvePosition2(rawX, xType, xScale);
|
|
5648
|
+
const cy = resolvePosition2(rawY, yType, yScale);
|
|
5649
|
+
if (cx === void 0 || cy === void 0) continue;
|
|
5524
5650
|
const category = colorField && !isSequentialColor ? String(row[colorField] ?? "") : void 0;
|
|
5525
5651
|
let color2;
|
|
5526
5652
|
if (isSequentialColor && colorField) {
|
|
@@ -5536,7 +5662,7 @@ function computeScatterMarks(spec, scales, _chartArea, _strategy) {
|
|
|
5536
5662
|
radius = sizeScale(sizeVal);
|
|
5537
5663
|
}
|
|
5538
5664
|
}
|
|
5539
|
-
const labelParts = [`${xChannel.field}=${
|
|
5665
|
+
const labelParts = [`${xChannel.field}=${rawX}`, `${yChannel.field}=${rawY}`];
|
|
5540
5666
|
if (category) labelParts.push(`${colorField}=${category}`);
|
|
5541
5667
|
if (sizeField && row[sizeField] != null) {
|
|
5542
5668
|
labelParts.push(`${sizeField}=${row[sizeField]}`);
|
|
@@ -6026,6 +6152,38 @@ function validateChartSpec(spec, errors) {
|
|
|
6026
6152
|
}
|
|
6027
6153
|
for (const [channel, channelSpec] of Object.entries(encoding)) {
|
|
6028
6154
|
if (!channelSpec || typeof channelSpec !== "object") continue;
|
|
6155
|
+
if (channel === "tooltip" && Array.isArray(channelSpec)) {
|
|
6156
|
+
for (let i = 0; i < channelSpec.length; i++) {
|
|
6157
|
+
const elem = channelSpec[i];
|
|
6158
|
+
if (!elem || typeof elem !== "object") continue;
|
|
6159
|
+
if (!elem.field || typeof elem.field !== "string") {
|
|
6160
|
+
errors.push({
|
|
6161
|
+
message: `Spec error: encoding.tooltip[${i}] must have a "field" string`,
|
|
6162
|
+
path: `encoding.tooltip[${i}].field`,
|
|
6163
|
+
code: "MISSING_FIELD",
|
|
6164
|
+
suggestion: `Add a field name from your data columns: ${availableColumns}`
|
|
6165
|
+
});
|
|
6166
|
+
continue;
|
|
6167
|
+
}
|
|
6168
|
+
if (!dataColumns.has(elem.field) && !transformFields.has(elem.field)) {
|
|
6169
|
+
errors.push({
|
|
6170
|
+
message: `Spec error: encoding.tooltip[${i}].field "${elem.field}" does not exist in data. Available columns: ${availableColumns}`,
|
|
6171
|
+
path: `encoding.tooltip[${i}].field`,
|
|
6172
|
+
code: "DATA_FIELD_MISSING",
|
|
6173
|
+
suggestion: `Use one of the available data columns: ${availableColumns}`
|
|
6174
|
+
});
|
|
6175
|
+
}
|
|
6176
|
+
if (elem.type && !VALID_FIELD_TYPES.has(elem.type)) {
|
|
6177
|
+
errors.push({
|
|
6178
|
+
message: `Spec error: encoding.tooltip[${i}].type "${elem.type}" is not valid. Must be one of: ${[...VALID_FIELD_TYPES].join(", ")}`,
|
|
6179
|
+
path: `encoding.tooltip[${i}].type`,
|
|
6180
|
+
code: "INVALID_VALUE",
|
|
6181
|
+
suggestion: `Use one of: ${[...VALID_FIELD_TYPES].join(", ")}`
|
|
6182
|
+
});
|
|
6183
|
+
}
|
|
6184
|
+
}
|
|
6185
|
+
continue;
|
|
6186
|
+
}
|
|
6029
6187
|
const channelObj = channelSpec;
|
|
6030
6188
|
const channelRule = rules[channel];
|
|
6031
6189
|
if ("condition" in channelObj) continue;
|
|
@@ -7004,7 +7162,8 @@ function formatTickLabel(value, resolvedScale) {
|
|
|
7004
7162
|
if (TEMPORAL_SCALE_TYPES.has(resolvedScale.type)) {
|
|
7005
7163
|
const temporalFmt = buildTemporalFormatter(formatStr);
|
|
7006
7164
|
if (temporalFmt) return temporalFmt(value);
|
|
7007
|
-
|
|
7165
|
+
const useUtc = resolvedScale.type === "utc";
|
|
7166
|
+
return formatDate(value, void 0, void 0, useUtc);
|
|
7008
7167
|
}
|
|
7009
7168
|
if (NUMERIC_SCALE_TYPES.has(resolvedScale.type)) {
|
|
7010
7169
|
const num = value;
|
|
@@ -7257,13 +7416,25 @@ function computeDimensions(spec, options, legendLayout, theme, strategy) {
|
|
|
7257
7416
|
for (const ann of spec.annotations) {
|
|
7258
7417
|
if (ann.type === "text" && String(ann.x) === maxXStr) {
|
|
7259
7418
|
const textWidth = estimateTextWidth8(ann.text, ann.fontSize ?? 11, ann.fontWeight ?? 600);
|
|
7260
|
-
|
|
7419
|
+
const dx = ann.offset?.dx ?? 0;
|
|
7420
|
+
const anchor = ann.anchor ?? "auto";
|
|
7421
|
+
const baseRightExtent = anchor === "left" ? textWidth : (
|
|
7422
|
+
// text is to the right of anchor
|
|
7423
|
+
anchor === "right" ? 0 : (
|
|
7424
|
+
// text is to the left of anchor
|
|
7425
|
+
textWidth / 2
|
|
7426
|
+
)
|
|
7427
|
+
);
|
|
7428
|
+
const rightOverflow = Math.max(0, baseRightExtent + dx);
|
|
7429
|
+
if (rightOverflow > 0) {
|
|
7430
|
+
margins.right = Math.max(margins.right, padding + rightOverflow + 12);
|
|
7431
|
+
}
|
|
7261
7432
|
}
|
|
7262
7433
|
}
|
|
7263
7434
|
}
|
|
7264
7435
|
}
|
|
7265
7436
|
if (encoding.y && !isRadial) {
|
|
7266
|
-
if (spec.markType === "bar" || spec.markType === "circle" || encoding.y.type === "nominal" || encoding.y.type === "ordinal") {
|
|
7437
|
+
if (spec.markType === "bar" || spec.markType === "circle" || spec.markType === "lollipop" || encoding.y.type === "nominal" || encoding.y.type === "ordinal") {
|
|
7267
7438
|
const yField = encoding.y.field;
|
|
7268
7439
|
let maxLabelWidth = 0;
|
|
7269
7440
|
for (const row of spec.data) {
|
|
@@ -7645,7 +7816,7 @@ function buildPositionalScale(channel, data, rangeStart, rangeEnd, chartType, ax
|
|
|
7645
7816
|
return buildLinearScale(channel, data, rangeStart, rangeEnd);
|
|
7646
7817
|
case "nominal":
|
|
7647
7818
|
case "ordinal":
|
|
7648
|
-
if (chartType === "bar" || chartType === "circle" && axis === "y") {
|
|
7819
|
+
if (chartType === "bar" || (chartType === "circle" || chartType === "lollipop") && axis === "y") {
|
|
7649
7820
|
return buildBandScale(channel, data, rangeStart, rangeEnd);
|
|
7650
7821
|
}
|
|
7651
7822
|
return buildPointScale(channel, data, rangeStart, rangeEnd);
|
|
@@ -7766,6 +7937,7 @@ function swatchShapeForType(markType) {
|
|
|
7766
7937
|
return "line";
|
|
7767
7938
|
case "point":
|
|
7768
7939
|
case "circle":
|
|
7940
|
+
case "lollipop":
|
|
7769
7941
|
return "circle";
|
|
7770
7942
|
default:
|
|
7771
7943
|
return "square";
|
|
@@ -7868,10 +8040,11 @@ function computeLegend(spec, strategy, theme, chartArea) {
|
|
|
7868
8040
|
const entryHeight = Math.max(SWATCH_SIZE2, labelStyle.fontSize * labelStyle.lineHeight);
|
|
7869
8041
|
const maxHeightRatio = strategy.legendMaxHeight > 0 ? strategy.legendMaxHeight : RIGHT_LEGEND_MAX_HEIGHT_RATIO;
|
|
7870
8042
|
const maxLegendHeight = chartArea.height * maxHeightRatio;
|
|
7871
|
-
const
|
|
8043
|
+
const maxFromSpace = Math.max(
|
|
7872
8044
|
1,
|
|
7873
8045
|
Math.floor((maxLegendHeight - LEGEND_PADDING * 2) / (entryHeight + 4))
|
|
7874
8046
|
);
|
|
8047
|
+
const maxEntries = spec.legend?.symbolLimit != null ? Math.min(Math.max(1, spec.legend.symbolLimit), maxFromSpace) : maxFromSpace;
|
|
7875
8048
|
if (entries.length > maxEntries) {
|
|
7876
8049
|
entries = truncateEntries(entries, maxEntries);
|
|
7877
8050
|
}
|
|
@@ -7896,7 +8069,14 @@ function computeLegend(spec, strategy, theme, chartArea) {
|
|
|
7896
8069
|
};
|
|
7897
8070
|
}
|
|
7898
8071
|
const availableWidth = chartArea.width - LEGEND_PADDING * 2 - BRAND_RESERVE_WIDTH;
|
|
7899
|
-
|
|
8072
|
+
if (spec.legend?.symbolLimit != null) {
|
|
8073
|
+
const limit = Math.max(1, spec.legend.symbolLimit);
|
|
8074
|
+
if (limit < entries.length) {
|
|
8075
|
+
entries = truncateEntries(entries, limit);
|
|
8076
|
+
}
|
|
8077
|
+
}
|
|
8078
|
+
const maxRows = spec.legend?.columns != null ? Math.ceil(entries.length / spec.legend.columns) : TOP_LEGEND_MAX_ROWS;
|
|
8079
|
+
const maxFit = entriesThatFit(entries, availableWidth, maxRows, labelStyle);
|
|
7900
8080
|
if (maxFit < entries.length) {
|
|
7901
8081
|
entries = truncateEntries(entries, maxFit);
|
|
7902
8082
|
}
|
|
@@ -8589,7 +8769,17 @@ function formatValue(value, fieldType, format2) {
|
|
|
8589
8769
|
}
|
|
8590
8770
|
return String(value);
|
|
8591
8771
|
}
|
|
8772
|
+
function buildExplicitTooltipFields(row, channels) {
|
|
8773
|
+
return channels.map((ch) => ({
|
|
8774
|
+
label: ch.axis?.label ?? ch.field,
|
|
8775
|
+
value: formatValue(row[ch.field], ch.type, ch.axis?.format)
|
|
8776
|
+
}));
|
|
8777
|
+
}
|
|
8592
8778
|
function buildFields(row, encoding, color2) {
|
|
8779
|
+
if (encoding.tooltip) {
|
|
8780
|
+
const channels = Array.isArray(encoding.tooltip) ? encoding.tooltip : [encoding.tooltip];
|
|
8781
|
+
return buildExplicitTooltipFields(row, channels);
|
|
8782
|
+
}
|
|
8593
8783
|
const fields = [];
|
|
8594
8784
|
if (encoding.y) {
|
|
8595
8785
|
fields.push({
|
|
@@ -8909,6 +9099,8 @@ var builtinRenderers = {
|
|
|
8909
9099
|
// old 'donut'
|
|
8910
9100
|
circle: dotRenderer,
|
|
8911
9101
|
// old 'dot'
|
|
9102
|
+
lollipop: dotRenderer,
|
|
9103
|
+
// semantic alias for dot/circle
|
|
8912
9104
|
text: textRenderer,
|
|
8913
9105
|
rule: ruleRenderer,
|
|
8914
9106
|
tick: tickRenderer,
|
|
@@ -9144,7 +9336,8 @@ function compileChart(spec, options) {
|
|
|
9144
9336
|
chartArea,
|
|
9145
9337
|
strategy,
|
|
9146
9338
|
theme.isDark,
|
|
9147
|
-
obstacles
|
|
9339
|
+
obstacles,
|
|
9340
|
+
{ width: dims.total.width, height: dims.total.height }
|
|
9148
9341
|
);
|
|
9149
9342
|
const tooltipDescriptors = computeTooltipDescriptors(chartSpec, marks);
|
|
9150
9343
|
const altText = generateAltText(
|