@opendata-ai/openchart-core 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.d.ts +73 -5
- package/dist/index.js +50 -6
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +8 -1
- package/src/labels/__tests__/collision.test.ts +67 -2
- package/src/labels/collision.ts +27 -3
- package/src/labels/index.ts +8 -2
- package/src/locale/__tests__/format.test.ts +8 -0
- package/src/types/__tests__/encoding.test.ts +33 -1
- package/src/types/__tests__/spec.test.ts +4 -2
- package/src/types/encoding.ts +13 -2
- package/src/types/events.ts +43 -3
- package/src/types/index.ts +2 -0
- package/src/types/layout.ts +2 -0
- package/src/types/spec.ts +10 -1
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -88,8 +88,15 @@ export {
|
|
|
88
88
|
export type {
|
|
89
89
|
LabelCandidate,
|
|
90
90
|
LabelPriority,
|
|
91
|
+
OffsetStrategy,
|
|
92
|
+
} from './labels/index';
|
|
93
|
+
export {
|
|
94
|
+
computeLabelBounds,
|
|
95
|
+
detectCollision,
|
|
96
|
+
EXTENDED_OFFSET_STRATEGIES,
|
|
97
|
+
OFFSET_STRATEGIES,
|
|
98
|
+
resolveCollisions,
|
|
91
99
|
} from './labels/index';
|
|
92
|
-
export { computeLabelBounds, detectCollision, resolveCollisions } from './labels/index';
|
|
93
100
|
|
|
94
101
|
// ---------------------------------------------------------------------------
|
|
95
102
|
// Locale: number and date formatting
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
2
|
import type { TextStyle } from '../../types/layout';
|
|
3
|
-
import
|
|
4
|
-
|
|
3
|
+
import {
|
|
4
|
+
detectCollision,
|
|
5
|
+
EXTENDED_OFFSET_STRATEGIES,
|
|
6
|
+
type LabelCandidate,
|
|
7
|
+
OFFSET_STRATEGIES,
|
|
8
|
+
resolveCollisions,
|
|
9
|
+
} from '../collision';
|
|
5
10
|
|
|
6
11
|
const defaultStyle: TextStyle = {
|
|
7
12
|
fontFamily: 'Inter',
|
|
@@ -194,4 +199,64 @@ describe('resolveCollisions', () => {
|
|
|
194
199
|
it('handles empty input', () => {
|
|
195
200
|
expect(resolveCollisions([])).toEqual([]);
|
|
196
201
|
});
|
|
202
|
+
|
|
203
|
+
it('accepts custom strategies without breaking default behavior', () => {
|
|
204
|
+
const labels: LabelCandidate[] = [
|
|
205
|
+
{
|
|
206
|
+
text: 'A',
|
|
207
|
+
anchorX: 0,
|
|
208
|
+
anchorY: 0,
|
|
209
|
+
width: 20,
|
|
210
|
+
height: 14,
|
|
211
|
+
priority: 'data',
|
|
212
|
+
style: defaultStyle,
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
text: 'B',
|
|
216
|
+
anchorX: 100,
|
|
217
|
+
anchorY: 100,
|
|
218
|
+
width: 20,
|
|
219
|
+
height: 14,
|
|
220
|
+
priority: 'data',
|
|
221
|
+
style: defaultStyle,
|
|
222
|
+
},
|
|
223
|
+
];
|
|
224
|
+
|
|
225
|
+
// Explicit default strategies should behave the same as no parameter
|
|
226
|
+
const defaultResults = resolveCollisions(labels);
|
|
227
|
+
const explicitResults = resolveCollisions(labels, OFFSET_STRATEGIES);
|
|
228
|
+
expect(explicitResults).toEqual(defaultResults);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it('resolves more dense labels with extended strategies than default', () => {
|
|
232
|
+
// 10 labels at the exact same position, simulating many converging line endpoints.
|
|
233
|
+
// The default 7 strategies can place at most 7; extended strategies should place more.
|
|
234
|
+
const labels: LabelCandidate[] = Array.from({ length: 10 }, (_, i) => ({
|
|
235
|
+
text: `Series ${i}`,
|
|
236
|
+
anchorX: 400,
|
|
237
|
+
anchorY: 200,
|
|
238
|
+
width: 60,
|
|
239
|
+
height: 14,
|
|
240
|
+
priority: 'data' as const,
|
|
241
|
+
style: defaultStyle,
|
|
242
|
+
}));
|
|
243
|
+
|
|
244
|
+
const defaultResults = resolveCollisions(labels);
|
|
245
|
+
const extendedResults = resolveCollisions(labels, EXTENDED_OFFSET_STRATEGIES);
|
|
246
|
+
|
|
247
|
+
const defaultVisible = defaultResults.filter((r) => r.visible).length;
|
|
248
|
+
const extendedVisible = extendedResults.filter((r) => r.visible).length;
|
|
249
|
+
|
|
250
|
+
// Default has 7 strategies, so at most 7 can be placed
|
|
251
|
+
expect(defaultVisible).toBeLessThanOrEqual(OFFSET_STRATEGIES.length);
|
|
252
|
+
// Extended strategies should place more labels than default
|
|
253
|
+
expect(extendedVisible).toBeGreaterThan(defaultVisible);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it('EXTENDED_OFFSET_STRATEGIES includes all base strategies', () => {
|
|
257
|
+
for (const strategy of OFFSET_STRATEGIES) {
|
|
258
|
+
expect(EXTENDED_OFFSET_STRATEGIES).toContainEqual(strategy);
|
|
259
|
+
}
|
|
260
|
+
expect(EXTENDED_OFFSET_STRATEGIES.length).toBeGreaterThan(OFFSET_STRATEGIES.length);
|
|
261
|
+
});
|
|
197
262
|
});
|
package/src/labels/collision.ts
CHANGED
|
@@ -59,8 +59,14 @@ export function detectCollision(a: Rect, b: Rect): boolean {
|
|
|
59
59
|
// Offset strategies
|
|
60
60
|
// ---------------------------------------------------------------------------
|
|
61
61
|
|
|
62
|
+
/** An offset position to try when resolving a label collision. */
|
|
63
|
+
export interface OffsetStrategy {
|
|
64
|
+
dx: number;
|
|
65
|
+
dy: number;
|
|
66
|
+
}
|
|
67
|
+
|
|
62
68
|
/** Offsets to try when a label collides with an existing placement. */
|
|
63
|
-
const OFFSET_STRATEGIES = [
|
|
69
|
+
export const OFFSET_STRATEGIES: readonly OffsetStrategy[] = [
|
|
64
70
|
{ dx: 0, dy: 0 }, // original position
|
|
65
71
|
{ dx: 0, dy: -1.2 }, // above (factor of height)
|
|
66
72
|
{ dx: 0, dy: 1.2 }, // below
|
|
@@ -70,6 +76,20 @@ const OFFSET_STRATEGIES = [
|
|
|
70
76
|
{ dx: -1.1, dy: -1.2 }, // upper-left
|
|
71
77
|
];
|
|
72
78
|
|
|
79
|
+
/**
|
|
80
|
+
* Extended offset strategies with additional vertical spread for dense
|
|
81
|
+
* multi-series endpoints (e.g. 5+ line series converging at similar y-values).
|
|
82
|
+
*/
|
|
83
|
+
export const EXTENDED_OFFSET_STRATEGIES: readonly OffsetStrategy[] = [
|
|
84
|
+
...OFFSET_STRATEGIES,
|
|
85
|
+
{ dx: 0, dy: -2.4 }, // further above
|
|
86
|
+
{ dx: 0, dy: 2.4 }, // further below
|
|
87
|
+
{ dx: 0, dy: -3.6 }, // even further above
|
|
88
|
+
{ dx: 0, dy: 3.6 }, // even further below
|
|
89
|
+
{ dx: 1.1, dy: -2.4 }, // upper-right far
|
|
90
|
+
{ dx: 1.1, dy: 2.4 }, // lower-right far
|
|
91
|
+
];
|
|
92
|
+
|
|
73
93
|
// ---------------------------------------------------------------------------
|
|
74
94
|
// Public API
|
|
75
95
|
// ---------------------------------------------------------------------------
|
|
@@ -83,9 +103,13 @@ const OFFSET_STRATEGIES = [
|
|
|
83
103
|
* demoted to tooltip-only (visible: false).
|
|
84
104
|
*
|
|
85
105
|
* @param labels - Array of label candidates to position.
|
|
106
|
+
* @param strategies - Optional offset strategies to use (defaults to OFFSET_STRATEGIES).
|
|
86
107
|
* @returns Array of resolved labels with computed positions and visibility.
|
|
87
108
|
*/
|
|
88
|
-
export function resolveCollisions(
|
|
109
|
+
export function resolveCollisions(
|
|
110
|
+
labels: LabelCandidate[],
|
|
111
|
+
strategies: readonly OffsetStrategy[] = OFFSET_STRATEGIES,
|
|
112
|
+
): ResolvedLabel[] {
|
|
89
113
|
// Sort by priority (highest first)
|
|
90
114
|
const sorted = [...labels].sort(
|
|
91
115
|
(a, b) => PRIORITY_ORDER[a.priority] - PRIORITY_ORDER[b.priority],
|
|
@@ -100,7 +124,7 @@ export function resolveCollisions(labels: LabelCandidate[]): ResolvedLabel[] {
|
|
|
100
124
|
let bestY = label.anchorY;
|
|
101
125
|
|
|
102
126
|
// Try each offset strategy
|
|
103
|
-
for (const offset of
|
|
127
|
+
for (const offset of strategies) {
|
|
104
128
|
const candidateX = label.anchorX + offset.dx * label.width;
|
|
105
129
|
const candidateY = label.anchorY + offset.dy * label.height;
|
|
106
130
|
const candidateRect: Rect = {
|
package/src/labels/index.ts
CHANGED
|
@@ -2,5 +2,11 @@
|
|
|
2
2
|
* Labels module barrel export.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
export type { LabelCandidate, LabelPriority } from './collision';
|
|
6
|
-
export {
|
|
5
|
+
export type { LabelCandidate, LabelPriority, OffsetStrategy } from './collision';
|
|
6
|
+
export {
|
|
7
|
+
computeLabelBounds,
|
|
8
|
+
detectCollision,
|
|
9
|
+
EXTENDED_OFFSET_STRATEGIES,
|
|
10
|
+
OFFSET_STRATEGIES,
|
|
11
|
+
resolveCollisions,
|
|
12
|
+
} from './collision';
|
|
@@ -102,6 +102,14 @@ describe('buildD3Formatter', () => {
|
|
|
102
102
|
it('returns null for completely invalid format', () => {
|
|
103
103
|
expect(buildD3Formatter('not-a-format!!!')).toBeNull();
|
|
104
104
|
});
|
|
105
|
+
|
|
106
|
+
it('$~s formats low thousands with SI suffix', () => {
|
|
107
|
+
const fmt = buildD3Formatter('$~s');
|
|
108
|
+
expect(fmt).not.toBeNull();
|
|
109
|
+
expect(fmt!(6000)).toBe('$6k');
|
|
110
|
+
expect(fmt!(7000)).toBe('$7k');
|
|
111
|
+
expect(fmt!(14000)).toBe('$14k');
|
|
112
|
+
});
|
|
105
113
|
});
|
|
106
114
|
|
|
107
115
|
describe('formatDate', () => {
|
|
@@ -27,7 +27,7 @@ function getOptionalChannels(markType: MarkType): string[] {
|
|
|
27
27
|
// ---------------------------------------------------------------------------
|
|
28
28
|
|
|
29
29
|
describe('MARK_ENCODING_RULES', () => {
|
|
30
|
-
it('has entries for all
|
|
30
|
+
it('has entries for all 11 mark types', () => {
|
|
31
31
|
const expectedTypes: MarkType[] = [
|
|
32
32
|
'bar',
|
|
33
33
|
'line',
|
|
@@ -39,6 +39,7 @@ describe('MARK_ENCODING_RULES', () => {
|
|
|
39
39
|
'rule',
|
|
40
40
|
'tick',
|
|
41
41
|
'rect',
|
|
42
|
+
'lollipop',
|
|
42
43
|
];
|
|
43
44
|
for (const type of expectedTypes) {
|
|
44
45
|
expect(MARK_ENCODING_RULES[type]).toBeDefined();
|
|
@@ -110,6 +111,17 @@ describe('line encoding rules', () => {
|
|
|
110
111
|
});
|
|
111
112
|
|
|
112
113
|
describe('point encoding rules', () => {
|
|
114
|
+
it('accepts all four field types on x and y', () => {
|
|
115
|
+
const rules = MARK_ENCODING_RULES.point;
|
|
116
|
+
for (const axis of ['x', 'y'] as const) {
|
|
117
|
+
expect(rules[axis].required).toBe(true);
|
|
118
|
+
expect(rules[axis].allowedTypes).toContain('quantitative');
|
|
119
|
+
expect(rules[axis].allowedTypes).toContain('temporal');
|
|
120
|
+
expect(rules[axis].allowedTypes).toContain('nominal');
|
|
121
|
+
expect(rules[axis].allowedTypes).toContain('ordinal');
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
|
|
113
125
|
it('supports shape as optional', () => {
|
|
114
126
|
expect(getOptionalChannels('point')).toContain('shape');
|
|
115
127
|
});
|
|
@@ -200,6 +212,23 @@ describe('rect encoding rules', () => {
|
|
|
200
212
|
});
|
|
201
213
|
});
|
|
202
214
|
|
|
215
|
+
describe('lollipop encoding rules', () => {
|
|
216
|
+
it('has the same rules as circle (semantic alias)', () => {
|
|
217
|
+
const lollipopRules = MARK_ENCODING_RULES.lollipop;
|
|
218
|
+
const circleRules = MARK_ENCODING_RULES.circle;
|
|
219
|
+
expect(lollipopRules).toEqual(circleRules);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it('requires x (quantitative) and y (nominal/ordinal)', () => {
|
|
223
|
+
const rules = MARK_ENCODING_RULES.lollipop;
|
|
224
|
+
expect(rules.x.required).toBe(true);
|
|
225
|
+
expect(rules.x.allowedTypes).toEqual(['quantitative']);
|
|
226
|
+
expect(rules.y.required).toBe(true);
|
|
227
|
+
expect(rules.y.allowedTypes).toContain('nominal');
|
|
228
|
+
expect(rules.y.allowedTypes).toContain('ordinal');
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
|
|
203
232
|
describe('common channels across marks', () => {
|
|
204
233
|
it('tooltip is optional on all mark types', () => {
|
|
205
234
|
const allTypes: MarkType[] = [
|
|
@@ -213,6 +242,7 @@ describe('common channels across marks', () => {
|
|
|
213
242
|
'rule',
|
|
214
243
|
'tick',
|
|
215
244
|
'rect',
|
|
245
|
+
'lollipop',
|
|
216
246
|
];
|
|
217
247
|
for (const type of allTypes) {
|
|
218
248
|
const rules = MARK_ENCODING_RULES[type];
|
|
@@ -234,6 +264,7 @@ describe('common channels across marks', () => {
|
|
|
234
264
|
'rule',
|
|
235
265
|
'tick',
|
|
236
266
|
'rect',
|
|
267
|
+
'lollipop',
|
|
237
268
|
];
|
|
238
269
|
for (const type of allTypes) {
|
|
239
270
|
const rules = MARK_ENCODING_RULES[type];
|
|
@@ -255,6 +286,7 @@ describe('common channels across marks', () => {
|
|
|
255
286
|
'rule',
|
|
256
287
|
'tick',
|
|
257
288
|
'rect',
|
|
289
|
+
'lollipop',
|
|
258
290
|
];
|
|
259
291
|
for (const type of allTypes) {
|
|
260
292
|
const rules = MARK_ENCODING_RULES[type];
|
|
@@ -81,6 +81,7 @@ describe('isChartSpec', () => {
|
|
|
81
81
|
'rule',
|
|
82
82
|
'tick',
|
|
83
83
|
'rect',
|
|
84
|
+
'lollipop',
|
|
84
85
|
] as const;
|
|
85
86
|
|
|
86
87
|
for (const markType of markTypes) {
|
|
@@ -151,8 +152,8 @@ describe('type guard mutual exclusivity', () => {
|
|
|
151
152
|
// ---------------------------------------------------------------------------
|
|
152
153
|
|
|
153
154
|
describe('MARK_TYPES', () => {
|
|
154
|
-
it('contains all
|
|
155
|
-
expect(MARK_TYPES.size).toBe(
|
|
155
|
+
it('contains all 11 mark types', () => {
|
|
156
|
+
expect(MARK_TYPES.size).toBe(11);
|
|
156
157
|
});
|
|
157
158
|
|
|
158
159
|
it('contains expected types', () => {
|
|
@@ -167,6 +168,7 @@ describe('MARK_TYPES', () => {
|
|
|
167
168
|
'rule',
|
|
168
169
|
'tick',
|
|
169
170
|
'rect',
|
|
171
|
+
'lollipop',
|
|
170
172
|
];
|
|
171
173
|
for (const t of expected) {
|
|
172
174
|
expect(MARK_TYPES.has(t)).toBe(true);
|
package/src/types/encoding.ts
CHANGED
|
@@ -110,8 +110,8 @@ export const MARK_ENCODING_RULES: Record<MarkType, EncodingRule> = {
|
|
|
110
110
|
detail: optional('nominal'),
|
|
111
111
|
},
|
|
112
112
|
point: {
|
|
113
|
-
x: required('quantitative'),
|
|
114
|
-
y: required('quantitative'),
|
|
113
|
+
x: required('quantitative', 'temporal', 'nominal', 'ordinal'),
|
|
114
|
+
y: required('quantitative', 'temporal', 'nominal', 'ordinal'),
|
|
115
115
|
color: optional('nominal', 'ordinal', 'quantitative'),
|
|
116
116
|
size: optional('quantitative'),
|
|
117
117
|
shape: optional('nominal', 'ordinal'),
|
|
@@ -132,6 +132,17 @@ export const MARK_ENCODING_RULES: Record<MarkType, EncodingRule> = {
|
|
|
132
132
|
order: optional('quantitative', 'ordinal'),
|
|
133
133
|
detail: optional('nominal'),
|
|
134
134
|
},
|
|
135
|
+
lollipop: {
|
|
136
|
+
x: required('quantitative'),
|
|
137
|
+
y: required('nominal', 'ordinal'),
|
|
138
|
+
color: optional('nominal', 'ordinal', 'quantitative'),
|
|
139
|
+
size: optional('quantitative'),
|
|
140
|
+
opacity: optional('quantitative'),
|
|
141
|
+
tooltip: optional(),
|
|
142
|
+
href: optional(),
|
|
143
|
+
order: optional('quantitative', 'ordinal'),
|
|
144
|
+
detail: optional('nominal'),
|
|
145
|
+
},
|
|
135
146
|
arc: {
|
|
136
147
|
x: optional(),
|
|
137
148
|
y: required('quantitative'),
|
package/src/types/events.ts
CHANGED
|
@@ -25,13 +25,45 @@ import type {
|
|
|
25
25
|
/** Identifies a specific chrome text element (title, subtitle, source, byline, footer). */
|
|
26
26
|
export type ChromeKey = 'title' | 'subtitle' | 'source' | 'byline' | 'footer';
|
|
27
27
|
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
// Element references (identity for selection/edit callbacks)
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Reference to an editable chart element.
|
|
34
|
+
* Carries enough info to find the element in both the spec and the DOM.
|
|
35
|
+
* The `annotation` variant uses two-tier identity: `id` (when the consumer provides one)
|
|
36
|
+
* and `index` (always available, position in the spec's annotations array).
|
|
37
|
+
*/
|
|
38
|
+
export type ElementRef =
|
|
39
|
+
| { type: 'annotation'; index: number; id?: string }
|
|
40
|
+
| { type: 'chrome'; key: ChromeKey }
|
|
41
|
+
| { type: 'series-label'; series: string }
|
|
42
|
+
| { type: 'legend' }
|
|
43
|
+
| { type: 'legend-entry'; series: string; index: number };
|
|
44
|
+
|
|
45
|
+
/** Helper constructors for ergonomic ElementRef creation. */
|
|
46
|
+
export const elementRef = {
|
|
47
|
+
annotation: (index: number, id?: string): ElementRef => ({ type: 'annotation', index, id }),
|
|
48
|
+
chrome: (key: ChromeKey): ElementRef => ({ type: 'chrome', key }),
|
|
49
|
+
seriesLabel: (series: string): ElementRef => ({ type: 'series-label', series }),
|
|
50
|
+
legend: (): ElementRef => ({ type: 'legend' }),
|
|
51
|
+
legendEntry: (series: string, index: number): ElementRef => ({
|
|
52
|
+
type: 'legend-entry',
|
|
53
|
+
series,
|
|
54
|
+
index,
|
|
55
|
+
}),
|
|
56
|
+
} as const;
|
|
57
|
+
|
|
28
58
|
// ---------------------------------------------------------------------------
|
|
29
59
|
// Element edit events
|
|
30
60
|
// ---------------------------------------------------------------------------
|
|
31
61
|
|
|
32
62
|
/**
|
|
33
63
|
* Discriminated union of all element edit events.
|
|
34
|
-
* Fired by the `onEdit` callback when any editable chart element is
|
|
64
|
+
* Fired by the `onEdit` callback when any editable chart element is modified.
|
|
65
|
+
* Covers repositioning (drag), deletion (Delete key), and text editing (double-click).
|
|
66
|
+
* Selection events (onSelect/onDeselect) are separate callbacks, not part of this union.
|
|
35
67
|
*/
|
|
36
68
|
export type ElementEdit =
|
|
37
69
|
| { type: 'annotation'; annotation: TextAnnotation; offset: AnnotationOffset }
|
|
@@ -46,7 +78,9 @@ export type ElementEdit =
|
|
|
46
78
|
| { type: 'chrome'; key: ChromeKey; text: string; offset: AnnotationOffset }
|
|
47
79
|
| { type: 'series-label'; series: string; offset: AnnotationOffset }
|
|
48
80
|
| { type: 'legend'; offset: AnnotationOffset }
|
|
49
|
-
| { type: 'legend-toggle'; series: string; hidden: boolean }
|
|
81
|
+
| { type: 'legend-toggle'; series: string; hidden: boolean }
|
|
82
|
+
| { type: 'delete'; element: ElementRef }
|
|
83
|
+
| { type: 'text-edit'; element: ElementRef; oldText: string; newText: string };
|
|
50
84
|
|
|
51
85
|
// ---------------------------------------------------------------------------
|
|
52
86
|
// Mark events
|
|
@@ -92,6 +126,12 @@ export interface ChartEventHandlers {
|
|
|
92
126
|
onAnnotationClick?: (annotation: Annotation, event: MouseEvent) => void;
|
|
93
127
|
/** Called when a text annotation label is dragged to a new position. */
|
|
94
128
|
onAnnotationEdit?: (annotation: TextAnnotation, updatedOffset: AnnotationOffset) => void;
|
|
95
|
-
/** Unified edit callback. Fires for any
|
|
129
|
+
/** Unified edit callback. Fires for any spec-modifying edit (repositioning, deletion, text editing). */
|
|
96
130
|
onEdit?: (edit: ElementEdit) => void;
|
|
131
|
+
/** Fired when an element is selected via click or programmatic select(). */
|
|
132
|
+
onSelect?: (element: ElementRef) => void;
|
|
133
|
+
/** Fired when the current element is deselected (click empty, Escape, or new selection replaces old). */
|
|
134
|
+
onDeselect?: (element: ElementRef) => void;
|
|
135
|
+
/** Fired when inline text editing commits. Also flows through onEdit as a 'text-edit' event. */
|
|
136
|
+
onTextEdit?: (element: ElementRef, oldText: string, newText: string) => void;
|
|
97
137
|
}
|
package/src/types/index.ts
CHANGED
package/src/types/layout.ts
CHANGED
|
@@ -490,6 +490,8 @@ export interface ResolvedLabel {
|
|
|
490
490
|
export interface ResolvedAnnotation {
|
|
491
491
|
/** Original annotation type. */
|
|
492
492
|
type: 'text' | 'range' | 'refline';
|
|
493
|
+
/** Stable identifier from the spec annotation, for selection/edit callbacks. */
|
|
494
|
+
id?: string;
|
|
493
495
|
/** Label text (if any). */
|
|
494
496
|
label?: ResolvedLabel;
|
|
495
497
|
/** For range: the highlighted rectangle in pixel coordinates. */
|
package/src/types/spec.ts
CHANGED
|
@@ -42,7 +42,8 @@ export type MarkType =
|
|
|
42
42
|
| 'text'
|
|
43
43
|
| 'rule'
|
|
44
44
|
| 'tick'
|
|
45
|
-
| 'rect'
|
|
45
|
+
| 'rect'
|
|
46
|
+
| 'lollipop';
|
|
46
47
|
|
|
47
48
|
/** @deprecated Use MarkType instead. Kept for internal migration references. */
|
|
48
49
|
export type ChartType = MarkType;
|
|
@@ -381,6 +382,8 @@ export type AnnotationAnchor = 'top' | 'bottom' | 'left' | 'right' | 'auto';
|
|
|
381
382
|
|
|
382
383
|
/** Base properties shared by all annotation types. */
|
|
383
384
|
interface AnnotationBase {
|
|
385
|
+
/** Stable identifier for selection and edit callbacks. When provided, edit events include this ID for reliable element matching. */
|
|
386
|
+
id?: string;
|
|
384
387
|
/** Human-readable label for the annotation. */
|
|
385
388
|
label?: string;
|
|
386
389
|
/** Fill color for the annotation element. */
|
|
@@ -567,6 +570,10 @@ export interface LegendConfig {
|
|
|
567
570
|
offset?: AnnotationOffset;
|
|
568
571
|
/** Whether to show the legend. Defaults to true. Set to false to hide. */
|
|
569
572
|
show?: boolean;
|
|
573
|
+
/** Number of columns for horizontal legend layout. Overrides the default row limit. */
|
|
574
|
+
columns?: number;
|
|
575
|
+
/** Max number of legend entries before truncation. Remaining entries show as "+N more". */
|
|
576
|
+
symbolLimit?: number;
|
|
570
577
|
}
|
|
571
578
|
|
|
572
579
|
// ---------------------------------------------------------------------------
|
|
@@ -1007,6 +1014,7 @@ export const MARK_TYPES: ReadonlySet<string> = new Set<MarkType>([
|
|
|
1007
1014
|
'rule',
|
|
1008
1015
|
'tick',
|
|
1009
1016
|
'rect',
|
|
1017
|
+
'lollipop',
|
|
1010
1018
|
]);
|
|
1011
1019
|
|
|
1012
1020
|
/** @deprecated Use MARK_TYPES instead. */
|
|
@@ -1085,4 +1093,5 @@ export const MARK_DISPLAY_NAMES: Record<MarkType, string> = {
|
|
|
1085
1093
|
rule: 'Rule chart',
|
|
1086
1094
|
tick: 'Tick plot',
|
|
1087
1095
|
rect: 'Heatmap',
|
|
1096
|
+
lollipop: 'Lollipop chart',
|
|
1088
1097
|
};
|