@opendata-ai/openchart-vanilla 8.0.0-rc.2 → 8.0.0-rc.21
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/{chunk-W34KN7FD.js → chunk-2NAQRP66.js} +2 -2
- package/dist/chunk-DCODCLWI.js +234 -0
- package/dist/chunk-DCODCLWI.js.map +1 -0
- package/dist/{chunk-5EOV5JQZ.js → chunk-EKAUQAAX.js} +50 -46
- package/dist/chunk-EKAUQAAX.js.map +1 -0
- package/dist/{chunk-ENXSUXC5.js → chunk-KNT5AZ2O.js} +7309 -5490
- package/dist/chunk-KNT5AZ2O.js.map +1 -0
- package/dist/chunk-RM5XS6NR.js +113 -0
- package/dist/chunk-RM5XS6NR.js.map +1 -0
- package/dist/chunk-T32UHMZA.js +282 -0
- package/dist/chunk-T32UHMZA.js.map +1 -0
- package/dist/export-gif.d.ts +113 -0
- package/dist/export-gif.js +21 -0
- package/dist/export-gif.js.map +1 -0
- package/dist/index.d.ts +283 -24
- package/dist/index.js +2012 -763
- package/dist/index.js.map +1 -1
- package/dist/{mount-CUKQLeqD.d.ts → mount-Dvv8NYIv.d.ts} +117 -3
- package/dist/simulation-worker.js +83 -2
- package/dist/static.js +7 -69
- package/dist/static.js.map +1 -1
- package/dist/story/index.d.ts +3 -2
- package/dist/story/index.js +5 -3
- package/dist/story/index.js.map +1 -1
- package/dist/styles.css +1 -1
- package/dist/styles.css.map +1 -1
- package/package.json +14 -6
- package/dist/chunk-5EOV5JQZ.js.map +0 -1
- package/dist/chunk-ENXSUXC5.js.map +0 -1
- package/src/__test-fixtures__/dom.ts +0 -70
- package/src/__test-fixtures__/specs.ts +0 -214
- package/src/__tests__/animation.test.ts +0 -358
- package/src/__tests__/annotations-render.test.ts +0 -201
- package/src/__tests__/compound-labels.test.ts +0 -122
- package/src/__tests__/continuous-legend.test.ts +0 -91
- package/src/__tests__/crosshair.test.ts +0 -130
- package/src/__tests__/edit-events.test.ts +0 -902
- package/src/__tests__/events.test.ts +0 -387
- package/src/__tests__/export.test.ts +0 -241
- package/src/__tests__/font-race.test.ts +0 -199
- package/src/__tests__/gradient-ids.test.ts +0 -159
- package/src/__tests__/gradient-utils.test.ts +0 -125
- package/src/__tests__/known-bugs-render.test.ts +0 -183
- package/src/__tests__/map.test.ts +0 -293
- package/src/__tests__/mount.test.ts +0 -357
- package/src/__tests__/pattern-fills.test.ts +0 -105
- package/src/__tests__/resize-timing.test.ts +0 -218
- package/src/__tests__/sankey.test.ts +0 -133
- package/src/__tests__/selection-events.test.ts +0 -869
- package/src/__tests__/series-search.test.ts +0 -311
- package/src/__tests__/static.test.ts +0 -141
- package/src/__tests__/svg-renderer.test.ts +0 -895
- package/src/__tests__/table-keyboard.test.ts +0 -361
- package/src/__tests__/table-mount.test.ts +0 -639
- package/src/__tests__/tilemap.test.ts +0 -158
- package/src/__tests__/tooltip.test.ts +0 -337
- package/src/__tests__/transition.test.ts +0 -1777
- package/src/__tests__/you-draw-it.test.ts +0 -273
- package/src/animation-vars.ts +0 -42
- package/src/animation.ts +0 -76
- package/src/barlist-mount.ts +0 -383
- package/src/barlist-renderer.ts +0 -267
- package/src/dom-helpers.ts +0 -17
- package/src/export.ts +0 -482
- package/src/gradient-utils.ts +0 -138
- package/src/graph/__tests__/canvas-renderer.test.ts +0 -713
- package/src/graph/__tests__/graph-mount.test.ts +0 -213
- package/src/graph/__tests__/interaction.test.ts +0 -205
- package/src/graph/__tests__/keyboard.test.ts +0 -653
- package/src/graph/__tests__/search.test.ts +0 -88
- package/src/graph/__tests__/simulation.test.ts +0 -233
- package/src/graph/__tests__/spatial-index.test.ts +0 -142
- package/src/graph/__tests__/zoom.test.ts +0 -203
- package/src/graph/canvas-renderer.ts +0 -747
- package/src/graph/interaction.ts +0 -367
- package/src/graph/keyboard.ts +0 -208
- package/src/graph/search.ts +0 -50
- package/src/graph/simulation-worker-url.ts +0 -12
- package/src/graph/simulation-worker.ts +0 -278
- package/src/graph/simulation.ts +0 -422
- package/src/graph/spatial-index.ts +0 -121
- package/src/graph/types.ts +0 -47
- package/src/graph/worker-protocol.ts +0 -70
- package/src/graph/zoom.ts +0 -134
- package/src/graph-mount.ts +0 -888
- package/src/index.ts +0 -76
- package/src/interactions/chart-events.ts +0 -139
- package/src/interactions/crosshair.ts +0 -233
- package/src/interactions/drag-handler.ts +0 -175
- package/src/interactions/editing-drags.ts +0 -515
- package/src/interactions/index.ts +0 -25
- package/src/interactions/keyboard-nav.ts +0 -111
- package/src/interactions/legend-interaction.ts +0 -46
- package/src/interactions/selection.ts +0 -280
- package/src/interactions/tooltip-events.ts +0 -72
- package/src/map-camera.ts +0 -88
- package/src/map-mount.ts +0 -701
- package/src/map-renderer.ts +0 -338
- package/src/map-transition.ts +0 -83
- package/src/measure-text.ts +0 -134
- package/src/mount.ts +0 -1382
- package/src/pattern-utils.ts +0 -110
- package/src/renderers/annotations.ts +0 -388
- package/src/renderers/axes.ts +0 -351
- package/src/renderers/brand.ts +0 -87
- package/src/renderers/chrome.ts +0 -196
- package/src/renderers/endpoint-labels.ts +0 -150
- package/src/renderers/legend.ts +0 -311
- package/src/renderers/marks.ts +0 -561
- package/src/renderers/metrics.ts +0 -63
- package/src/renderers/svg-dom.ts +0 -40
- package/src/renderers/table-cells.ts +0 -446
- package/src/resize-observer.ts +0 -46
- package/src/sankey-mount.ts +0 -622
- package/src/sankey-renderer.ts +0 -614
- package/src/series-search.ts +0 -392
- package/src/static.ts +0 -251
- package/src/story/__tests__/camera-math.test.ts +0 -145
- package/src/story/__tests__/create-chart-story.test.ts +0 -317
- package/src/story/__tests__/progress-math.test.ts +0 -95
- package/src/story/__tests__/scroll-driver.test.ts +0 -130
- package/src/story/__tests__/tween.test.ts +0 -159
- package/src/story/camera-math.ts +0 -125
- package/src/story/create-chart-story.ts +0 -353
- package/src/story/crossfade.ts +0 -70
- package/src/story/index.ts +0 -39
- package/src/story/progress-math.ts +0 -85
- package/src/story/resolve-camera-target.ts +0 -119
- package/src/story/scroll-driver.ts +0 -206
- package/src/story/story-camera.ts +0 -49
- package/src/story/tween.ts +0 -112
- package/src/story/types.ts +0 -76
- package/src/svg-ids.ts +0 -23
- package/src/svg-renderer.ts +0 -383
- package/src/table-keyboard.ts +0 -266
- package/src/table-mount.ts +0 -567
- package/src/table-renderer.ts +0 -414
- package/src/text-edit-overlay.ts +0 -255
- package/src/theme-tokens.ts +0 -51
- package/src/tilemap-mount.ts +0 -511
- package/src/tilemap-renderer.ts +0 -396
- package/src/tooltip.ts +0 -177
- package/src/transition.ts +0 -2816
- package/src/you-draw-it.ts +0 -480
- /package/dist/{chunk-W34KN7FD.js.map → chunk-2NAQRP66.js.map} +0 -0
|
@@ -1,902 +0,0 @@
|
|
|
1
|
-
import type { ChartSpec, ElementEdit } from '@opendata-ai/openchart-engine';
|
|
2
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
-
import { createContainer, createMouseEvent } from '../__test-fixtures__/dom';
|
|
4
|
-
import { barSpec, lineSpec } from '../__test-fixtures__/specs';
|
|
5
|
-
import { createChart } from '../mount';
|
|
6
|
-
|
|
7
|
-
// ---------------------------------------------------------------------------
|
|
8
|
-
// Shared specs
|
|
9
|
-
// ---------------------------------------------------------------------------
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Line chart with text annotation + connector, range, refline, chrome, legend,
|
|
13
|
-
* and multi-series data (which produces series labels).
|
|
14
|
-
*/
|
|
15
|
-
const fullEditSpec: ChartSpec = {
|
|
16
|
-
...lineSpec,
|
|
17
|
-
labels: { show: true },
|
|
18
|
-
annotations: [
|
|
19
|
-
{
|
|
20
|
-
type: 'text',
|
|
21
|
-
x: '2020-01-01',
|
|
22
|
-
y: 10,
|
|
23
|
-
text: 'Peak',
|
|
24
|
-
offset: { dx: 10, dy: -20 },
|
|
25
|
-
connector: true,
|
|
26
|
-
},
|
|
27
|
-
{ type: 'range', x1: '2020-01-01', x2: '2021-01-01', label: 'Boom Period' },
|
|
28
|
-
{ type: 'refline', y: 30, label: 'Target' },
|
|
29
|
-
],
|
|
30
|
-
chrome: {
|
|
31
|
-
title: 'GDP Growth',
|
|
32
|
-
subtitle: 'US vs UK over time',
|
|
33
|
-
source: 'World Bank',
|
|
34
|
-
},
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
/** Simple text annotation spec for focused annotation tests. */
|
|
38
|
-
const textAnnotatedSpec: ChartSpec = {
|
|
39
|
-
...barSpec,
|
|
40
|
-
annotations: [
|
|
41
|
-
{ type: 'text', x: 10, y: 'A', text: 'Peak', offset: { dx: 10, dy: -20 }, connector: true },
|
|
42
|
-
],
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Two annotations whose `zIndex` REVERSES their spec order.
|
|
47
|
-
*
|
|
48
|
-
* The engine sorts `layout.annotations` by zIndex before handing them to the
|
|
49
|
-
* renderer, so render position 0 here is spec annotation 1. Every other spec in
|
|
50
|
-
* this file has exactly one annotation, which makes `index === specIndex`
|
|
51
|
-
* trivially true and hides the difference entirely.
|
|
52
|
-
*/
|
|
53
|
-
const reorderedAnnotatedSpec: ChartSpec = {
|
|
54
|
-
...barSpec,
|
|
55
|
-
annotations: [
|
|
56
|
-
{ type: 'text', x: 10, y: 'A', text: 'First in the spec', id: 'first', zIndex: 5 },
|
|
57
|
-
{ type: 'text', x: 30, y: 'C', text: 'Second in the spec', id: 'second', zIndex: 1 },
|
|
58
|
-
],
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Curved connector, which is the ONLY path that draws an arrowhead — and the only
|
|
63
|
-
* one where edit mode has to find the connector's tip by parsing the arrowhead
|
|
64
|
-
* polyline (`wireConnectorEndpointDrag`'s curved branch). Every other spec in this
|
|
65
|
-
* file uses `connector: true` (straight, no arrow), so without this the arrowhead
|
|
66
|
-
* selector and the tip-parsing code never execute in any test.
|
|
67
|
-
*/
|
|
68
|
-
const curvedAnnotatedSpec: ChartSpec = {
|
|
69
|
-
...barSpec,
|
|
70
|
-
annotations: [
|
|
71
|
-
{
|
|
72
|
-
type: 'text',
|
|
73
|
-
x: 10,
|
|
74
|
-
y: 'A',
|
|
75
|
-
text: 'Peak',
|
|
76
|
-
offset: { dx: 40, dy: -50 },
|
|
77
|
-
connector: 'curve',
|
|
78
|
-
},
|
|
79
|
-
],
|
|
80
|
-
};
|
|
81
|
-
|
|
82
|
-
/** Range-only annotation spec. */
|
|
83
|
-
const rangeAnnotatedSpec: ChartSpec = {
|
|
84
|
-
...lineSpec,
|
|
85
|
-
annotations: [
|
|
86
|
-
{
|
|
87
|
-
type: 'range',
|
|
88
|
-
x1: '2020-01-01',
|
|
89
|
-
x2: '2021-01-01',
|
|
90
|
-
label: 'Boom Period',
|
|
91
|
-
labelOffset: { dx: 5, dy: 3 },
|
|
92
|
-
},
|
|
93
|
-
],
|
|
94
|
-
};
|
|
95
|
-
|
|
96
|
-
/** Refline-only annotation spec. */
|
|
97
|
-
const reflineAnnotatedSpec: ChartSpec = {
|
|
98
|
-
...lineSpec,
|
|
99
|
-
annotations: [{ type: 'refline', y: 30, label: 'Target', labelOffset: { dx: 2, dy: -4 } }],
|
|
100
|
-
};
|
|
101
|
-
|
|
102
|
-
// ---------------------------------------------------------------------------
|
|
103
|
-
// Helpers
|
|
104
|
-
// ---------------------------------------------------------------------------
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* Simulate a drag sequence: mousedown -> mousemove -> mouseup.
|
|
108
|
-
* Dispatches mousedown on the element, then mousemove/mouseup on document.
|
|
109
|
-
*/
|
|
110
|
-
function simulateDrag(
|
|
111
|
-
el: Element,
|
|
112
|
-
startX: number,
|
|
113
|
-
startY: number,
|
|
114
|
-
endX: number,
|
|
115
|
-
endY: number,
|
|
116
|
-
): void {
|
|
117
|
-
el.dispatchEvent(createMouseEvent('mousedown', startX, startY));
|
|
118
|
-
document.dispatchEvent(createMouseEvent('mousemove', endX, endY));
|
|
119
|
-
document.dispatchEvent(createMouseEvent('mouseup', endX, endY));
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
// ---------------------------------------------------------------------------
|
|
123
|
-
// Tests
|
|
124
|
-
// ---------------------------------------------------------------------------
|
|
125
|
-
|
|
126
|
-
describe('edit events', () => {
|
|
127
|
-
let container: HTMLDivElement;
|
|
128
|
-
|
|
129
|
-
beforeEach(() => {
|
|
130
|
-
container = createContainer();
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
afterEach(() => {
|
|
134
|
-
document.body.innerHTML = '';
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
// =========================================================================
|
|
138
|
-
// 1. createDragHandler utility (tested through annotation drag)
|
|
139
|
-
// =========================================================================
|
|
140
|
-
describe('createDragHandler utility (via annotation drag)', () => {
|
|
141
|
-
it('sets cursor:grab on the element', () => {
|
|
142
|
-
const onEdit = vi.fn();
|
|
143
|
-
const chart = createChart(container, textAnnotatedSpec, { onEdit });
|
|
144
|
-
|
|
145
|
-
const annotation = container.querySelector('.oc-annotation-text') as SVGGElement | null;
|
|
146
|
-
if (!annotation) {
|
|
147
|
-
chart.destroy();
|
|
148
|
-
return;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
expect(annotation.style.cursor).toBe('grab');
|
|
152
|
-
chart.destroy();
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
it('fires onEnd with correct dx/dy after drag', () => {
|
|
156
|
-
const onEdit = vi.fn();
|
|
157
|
-
const chart = createChart(container, textAnnotatedSpec, { onEdit });
|
|
158
|
-
|
|
159
|
-
const annotation = container.querySelector('.oc-annotation-text') as SVGGElement | null;
|
|
160
|
-
if (!annotation) {
|
|
161
|
-
chart.destroy();
|
|
162
|
-
return;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
simulateDrag(annotation, 100, 100, 150, 130);
|
|
166
|
-
|
|
167
|
-
expect(onEdit).toHaveBeenCalledTimes(1);
|
|
168
|
-
const edit: ElementEdit = onEdit.mock.calls[0][0];
|
|
169
|
-
expect(edit.type).toBe('annotation');
|
|
170
|
-
if (edit.type === 'annotation') {
|
|
171
|
-
// Original offset (10, -20) + drag delta (50, 30) = (60, 10)
|
|
172
|
-
expect(edit.offset.dx).toBe(60);
|
|
173
|
-
expect(edit.offset.dy).toBe(10);
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
chart.destroy();
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
it('does NOT fire onEnd when movement is below threshold (3px)', () => {
|
|
180
|
-
const onEdit = vi.fn();
|
|
181
|
-
const chart = createChart(container, textAnnotatedSpec, { onEdit });
|
|
182
|
-
|
|
183
|
-
const annotation = container.querySelector('.oc-annotation-text') as SVGGElement | null;
|
|
184
|
-
if (!annotation) {
|
|
185
|
-
chart.destroy();
|
|
186
|
-
return;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
// Move less than 3px
|
|
190
|
-
simulateDrag(annotation, 100, 100, 101, 101);
|
|
191
|
-
|
|
192
|
-
expect(onEdit).not.toHaveBeenCalled();
|
|
193
|
-
chart.destroy();
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
it('suppresses click event after real drag', () => {
|
|
197
|
-
const onAnnotationClick = vi.fn();
|
|
198
|
-
const onEdit = vi.fn();
|
|
199
|
-
const chart = createChart(container, textAnnotatedSpec, { onAnnotationClick, onEdit });
|
|
200
|
-
|
|
201
|
-
const annotation = container.querySelector('.oc-annotation-text') as SVGGElement | null;
|
|
202
|
-
if (!annotation) {
|
|
203
|
-
chart.destroy();
|
|
204
|
-
return;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
// Drag beyond threshold
|
|
208
|
-
simulateDrag(annotation, 100, 100, 150, 130);
|
|
209
|
-
|
|
210
|
-
// Fire a click right after drag
|
|
211
|
-
annotation.dispatchEvent(createMouseEvent('click', 150, 130));
|
|
212
|
-
|
|
213
|
-
expect(onEdit).toHaveBeenCalledTimes(1);
|
|
214
|
-
// Click should be suppressed
|
|
215
|
-
expect(onAnnotationClick).not.toHaveBeenCalled();
|
|
216
|
-
|
|
217
|
-
chart.destroy();
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
it('sets cursor:grabbing during drag', () => {
|
|
221
|
-
const onEdit = vi.fn();
|
|
222
|
-
const chart = createChart(container, textAnnotatedSpec, { onEdit });
|
|
223
|
-
|
|
224
|
-
const annotation = container.querySelector('.oc-annotation-text') as SVGGElement | null;
|
|
225
|
-
if (!annotation) {
|
|
226
|
-
chart.destroy();
|
|
227
|
-
return;
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
annotation.dispatchEvent(createMouseEvent('mousedown', 100, 100));
|
|
231
|
-
// During drag (after mousedown, before mouseup) the cursor should be grabbing
|
|
232
|
-
expect(annotation.style.cursor).toBe('grabbing');
|
|
233
|
-
|
|
234
|
-
// Complete the drag to clean up
|
|
235
|
-
document.dispatchEvent(createMouseEvent('mousemove', 120, 110));
|
|
236
|
-
document.dispatchEvent(createMouseEvent('mouseup', 120, 110));
|
|
237
|
-
|
|
238
|
-
// After drag, cursor should revert to grab
|
|
239
|
-
expect(annotation.style.cursor).toBe('grab');
|
|
240
|
-
|
|
241
|
-
chart.destroy();
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
it('cleanup function removes all listeners', () => {
|
|
245
|
-
const onEdit = vi.fn();
|
|
246
|
-
const chart = createChart(container, textAnnotatedSpec, { onEdit });
|
|
247
|
-
|
|
248
|
-
chart.destroy();
|
|
249
|
-
|
|
250
|
-
// After destroy, dispatching events should not fire the callback
|
|
251
|
-
document.dispatchEvent(createMouseEvent('mousemove', 200, 200));
|
|
252
|
-
document.dispatchEvent(createMouseEvent('mouseup', 200, 200));
|
|
253
|
-
|
|
254
|
-
expect(onEdit).not.toHaveBeenCalled();
|
|
255
|
-
});
|
|
256
|
-
});
|
|
257
|
-
|
|
258
|
-
// =========================================================================
|
|
259
|
-
// 2. onEdit callback integration
|
|
260
|
-
// =========================================================================
|
|
261
|
-
describe('onEdit callback integration', () => {
|
|
262
|
-
it('when onEdit is NOT provided, no grab cursors appear on chrome/legend/labels', () => {
|
|
263
|
-
// Mount without onEdit
|
|
264
|
-
const chart = createChart(container, fullEditSpec, {});
|
|
265
|
-
|
|
266
|
-
// Chrome elements should not have grab cursor
|
|
267
|
-
const chromeTexts = container.querySelectorAll('.oc-chrome text[data-chrome-key]');
|
|
268
|
-
for (const el of chromeTexts) {
|
|
269
|
-
expect((el as HTMLElement).style.cursor).not.toBe('grab');
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
// Legend should not have grab cursor
|
|
273
|
-
const legendG = container.querySelector('.oc-legend') as SVGGElement | null;
|
|
274
|
-
if (legendG) {
|
|
275
|
-
expect(legendG.style.cursor).not.toBe('grab');
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
// Series labels should not have grab cursor
|
|
279
|
-
const seriesLabels = container.querySelectorAll('.oc-mark-label');
|
|
280
|
-
for (const el of seriesLabels) {
|
|
281
|
-
expect((el as HTMLElement).style.cursor).not.toBe('grab');
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
chart.destroy();
|
|
285
|
-
});
|
|
286
|
-
|
|
287
|
-
it('when onEdit IS provided, grab cursors appear on editable elements', () => {
|
|
288
|
-
const onEdit = vi.fn();
|
|
289
|
-
const chart = createChart(container, fullEditSpec, { onEdit });
|
|
290
|
-
|
|
291
|
-
// Chrome elements should have grab cursor
|
|
292
|
-
const chromeTexts = container.querySelectorAll('.oc-chrome text[data-chrome-key]');
|
|
293
|
-
for (const el of chromeTexts) {
|
|
294
|
-
expect((el as HTMLElement).style.cursor).toBe('grab');
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
// Legend should have grab cursor (if present)
|
|
298
|
-
const legendG = container.querySelector('.oc-legend') as SVGGElement | null;
|
|
299
|
-
if (legendG) {
|
|
300
|
-
expect(legendG.style.cursor).toBe('grab');
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
// Series labels should have grab cursor (if any rendered with data-series)
|
|
304
|
-
const seriesLabels = container.querySelectorAll('.oc-mark-label[data-series]');
|
|
305
|
-
for (const el of seriesLabels) {
|
|
306
|
-
expect((el as HTMLElement).style.cursor).toBe('grab');
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
chart.destroy();
|
|
310
|
-
});
|
|
311
|
-
|
|
312
|
-
it('dragging a text annotation fires BOTH onAnnotationEdit and onEdit when both are provided', () => {
|
|
313
|
-
const onAnnotationEdit = vi.fn();
|
|
314
|
-
const onEdit = vi.fn();
|
|
315
|
-
const chart = createChart(container, textAnnotatedSpec, { onAnnotationEdit, onEdit });
|
|
316
|
-
|
|
317
|
-
const annotation = container.querySelector('.oc-annotation-text') as SVGGElement | null;
|
|
318
|
-
if (!annotation) {
|
|
319
|
-
chart.destroy();
|
|
320
|
-
return;
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
simulateDrag(annotation, 100, 100, 150, 130);
|
|
324
|
-
|
|
325
|
-
expect(onAnnotationEdit).toHaveBeenCalledTimes(1);
|
|
326
|
-
expect(onEdit).toHaveBeenCalledTimes(1);
|
|
327
|
-
|
|
328
|
-
// Verify onAnnotationEdit received the text annotation and offset
|
|
329
|
-
const [annotationArg, offsetArg] = onAnnotationEdit.mock.calls[0];
|
|
330
|
-
expect(annotationArg.type).toBe('text');
|
|
331
|
-
expect(annotationArg.text).toBe('Peak');
|
|
332
|
-
expect(offsetArg.dx).toBe(60);
|
|
333
|
-
expect(offsetArg.dy).toBe(10);
|
|
334
|
-
|
|
335
|
-
// Verify onEdit received an annotation edit
|
|
336
|
-
const edit: ElementEdit = onEdit.mock.calls[0][0];
|
|
337
|
-
expect(edit.type).toBe('annotation');
|
|
338
|
-
if (edit.type === 'annotation') {
|
|
339
|
-
expect(edit.annotation.text).toBe('Peak');
|
|
340
|
-
expect(edit.offset.dx).toBe(60);
|
|
341
|
-
expect(edit.offset.dy).toBe(10);
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
chart.destroy();
|
|
345
|
-
});
|
|
346
|
-
|
|
347
|
-
it('dragging a text annotation fires only onEdit when only onEdit is provided', () => {
|
|
348
|
-
const onEdit = vi.fn();
|
|
349
|
-
const chart = createChart(container, textAnnotatedSpec, { onEdit });
|
|
350
|
-
|
|
351
|
-
const annotation = container.querySelector('.oc-annotation-text') as SVGGElement | null;
|
|
352
|
-
if (!annotation) {
|
|
353
|
-
chart.destroy();
|
|
354
|
-
return;
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
simulateDrag(annotation, 100, 100, 150, 130);
|
|
358
|
-
|
|
359
|
-
expect(onEdit).toHaveBeenCalledTimes(1);
|
|
360
|
-
const edit: ElementEdit = onEdit.mock.calls[0][0];
|
|
361
|
-
expect(edit.type).toBe('annotation');
|
|
362
|
-
|
|
363
|
-
chart.destroy();
|
|
364
|
-
});
|
|
365
|
-
});
|
|
366
|
-
|
|
367
|
-
// =========================================================================
|
|
368
|
-
// 3. wireAnnotationLabelDrag (range/refline)
|
|
369
|
-
// =========================================================================
|
|
370
|
-
describe('wireAnnotationLabelDrag', () => {
|
|
371
|
-
it('range annotation labels get cursor:grab when onEdit is provided', () => {
|
|
372
|
-
const onEdit = vi.fn();
|
|
373
|
-
const chart = createChart(container, rangeAnnotatedSpec, { onEdit });
|
|
374
|
-
|
|
375
|
-
const rangeLabel = container.querySelector(
|
|
376
|
-
'.oc-annotation-range .oc-annotation-label',
|
|
377
|
-
) as SVGTextElement | null;
|
|
378
|
-
if (!rangeLabel) {
|
|
379
|
-
// Range may not render a visible label in test env
|
|
380
|
-
chart.destroy();
|
|
381
|
-
return;
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
expect(rangeLabel.style.cursor).toBe('grab');
|
|
385
|
-
chart.destroy();
|
|
386
|
-
});
|
|
387
|
-
|
|
388
|
-
it('dragging a range label fires onEdit with type range-label and accumulated labelOffset', () => {
|
|
389
|
-
const onEdit = vi.fn();
|
|
390
|
-
const chart = createChart(container, rangeAnnotatedSpec, { onEdit });
|
|
391
|
-
|
|
392
|
-
const rangeLabel = container.querySelector(
|
|
393
|
-
'.oc-annotation-range .oc-annotation-label',
|
|
394
|
-
) as SVGTextElement | null;
|
|
395
|
-
if (!rangeLabel) {
|
|
396
|
-
chart.destroy();
|
|
397
|
-
return;
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
simulateDrag(rangeLabel, 100, 100, 140, 120);
|
|
401
|
-
|
|
402
|
-
expect(onEdit).toHaveBeenCalledTimes(1);
|
|
403
|
-
const edit: ElementEdit = onEdit.mock.calls[0][0];
|
|
404
|
-
expect(edit.type).toBe('range-label');
|
|
405
|
-
if (edit.type === 'range-label') {
|
|
406
|
-
expect(edit.annotation.type).toBe('range');
|
|
407
|
-
// Original labelOffset (5, 3) + drag delta (40, 20) = (45, 23)
|
|
408
|
-
expect(edit.labelOffset.dx).toBe(45);
|
|
409
|
-
expect(edit.labelOffset.dy).toBe(23);
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
chart.destroy();
|
|
413
|
-
});
|
|
414
|
-
|
|
415
|
-
it('refline annotation labels get cursor:grab when onEdit is provided', () => {
|
|
416
|
-
const onEdit = vi.fn();
|
|
417
|
-
const chart = createChart(container, reflineAnnotatedSpec, { onEdit });
|
|
418
|
-
|
|
419
|
-
const reflineLabel = container.querySelector(
|
|
420
|
-
'.oc-annotation-refline .oc-annotation-label',
|
|
421
|
-
) as SVGTextElement | null;
|
|
422
|
-
if (!reflineLabel) {
|
|
423
|
-
chart.destroy();
|
|
424
|
-
return;
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
expect(reflineLabel.style.cursor).toBe('grab');
|
|
428
|
-
chart.destroy();
|
|
429
|
-
});
|
|
430
|
-
|
|
431
|
-
it('dragging a refline label fires onEdit with type refline-label and accumulated labelOffset', () => {
|
|
432
|
-
const onEdit = vi.fn();
|
|
433
|
-
const chart = createChart(container, reflineAnnotatedSpec, { onEdit });
|
|
434
|
-
|
|
435
|
-
const reflineLabel = container.querySelector(
|
|
436
|
-
'.oc-annotation-refline .oc-annotation-label',
|
|
437
|
-
) as SVGTextElement | null;
|
|
438
|
-
if (!reflineLabel) {
|
|
439
|
-
chart.destroy();
|
|
440
|
-
return;
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
simulateDrag(reflineLabel, 100, 100, 130, 115);
|
|
444
|
-
|
|
445
|
-
expect(onEdit).toHaveBeenCalledTimes(1);
|
|
446
|
-
const edit: ElementEdit = onEdit.mock.calls[0][0];
|
|
447
|
-
expect(edit.type).toBe('refline-label');
|
|
448
|
-
if (edit.type === 'refline-label') {
|
|
449
|
-
expect(edit.annotation.type).toBe('refline');
|
|
450
|
-
// Original labelOffset (2, -4) + drag delta (30, 15) = (32, 11)
|
|
451
|
-
expect(edit.labelOffset.dx).toBe(32);
|
|
452
|
-
expect(edit.labelOffset.dy).toBe(11);
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
chart.destroy();
|
|
456
|
-
});
|
|
457
|
-
});
|
|
458
|
-
|
|
459
|
-
// =========================================================================
|
|
460
|
-
// 4. wireChromeDrag
|
|
461
|
-
// =========================================================================
|
|
462
|
-
describe('wireChromeDrag', () => {
|
|
463
|
-
it('chrome text elements get cursor:grab when onEdit is provided', () => {
|
|
464
|
-
const onEdit = vi.fn();
|
|
465
|
-
const chart = createChart(container, fullEditSpec, { onEdit });
|
|
466
|
-
|
|
467
|
-
const chromeTexts = container.querySelectorAll('.oc-chrome text[data-chrome-key]');
|
|
468
|
-
expect(chromeTexts.length).toBeGreaterThan(0);
|
|
469
|
-
|
|
470
|
-
for (const el of chromeTexts) {
|
|
471
|
-
expect((el as HTMLElement).style.cursor).toBe('grab');
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
chart.destroy();
|
|
475
|
-
});
|
|
476
|
-
|
|
477
|
-
it('dragging a chrome element fires onEdit with type chrome, correct key, text, and accumulated offset', () => {
|
|
478
|
-
const onEdit = vi.fn();
|
|
479
|
-
const chart = createChart(container, fullEditSpec, { onEdit });
|
|
480
|
-
|
|
481
|
-
const titleEl = container.querySelector(
|
|
482
|
-
'.oc-chrome text[data-chrome-key="title"]',
|
|
483
|
-
) as SVGTextElement | null;
|
|
484
|
-
if (!titleEl) {
|
|
485
|
-
chart.destroy();
|
|
486
|
-
return;
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
simulateDrag(titleEl, 100, 100, 130, 115);
|
|
490
|
-
|
|
491
|
-
expect(onEdit).toHaveBeenCalledTimes(1);
|
|
492
|
-
const edit: ElementEdit = onEdit.mock.calls[0][0];
|
|
493
|
-
expect(edit.type).toBe('chrome');
|
|
494
|
-
if (edit.type === 'chrome') {
|
|
495
|
-
expect(edit.key).toBe('title');
|
|
496
|
-
expect(edit.text).toBeTruthy();
|
|
497
|
-
// No existing offset, so offset = delta (30, 15)
|
|
498
|
-
expect(edit.offset.dx).toBe(30);
|
|
499
|
-
expect(edit.offset.dy).toBe(15);
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
chart.destroy();
|
|
503
|
-
});
|
|
504
|
-
});
|
|
505
|
-
|
|
506
|
-
// =========================================================================
|
|
507
|
-
// 5. wireLegendDrag
|
|
508
|
-
// =========================================================================
|
|
509
|
-
describe('wireLegendDrag', () => {
|
|
510
|
-
it('legend group gets cursor:grab when onEdit is provided', () => {
|
|
511
|
-
const onEdit = vi.fn();
|
|
512
|
-
const chart = createChart(container, fullEditSpec, { onEdit });
|
|
513
|
-
|
|
514
|
-
const legendG = container.querySelector('.oc-legend') as SVGGElement | null;
|
|
515
|
-
if (!legendG) {
|
|
516
|
-
chart.destroy();
|
|
517
|
-
return;
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
expect(legendG.style.cursor).toBe('grab');
|
|
521
|
-
chart.destroy();
|
|
522
|
-
});
|
|
523
|
-
|
|
524
|
-
it('dragging legend fires onEdit with type legend and accumulated offset', () => {
|
|
525
|
-
const onEdit = vi.fn();
|
|
526
|
-
const chart = createChart(container, fullEditSpec, { onEdit });
|
|
527
|
-
|
|
528
|
-
const legendG = container.querySelector('.oc-legend') as SVGGElement | null;
|
|
529
|
-
if (!legendG) {
|
|
530
|
-
chart.destroy();
|
|
531
|
-
return;
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
simulateDrag(legendG, 200, 50, 250, 70);
|
|
535
|
-
|
|
536
|
-
expect(onEdit).toHaveBeenCalledTimes(1);
|
|
537
|
-
const edit: ElementEdit = onEdit.mock.calls[0][0];
|
|
538
|
-
expect(edit.type).toBe('legend');
|
|
539
|
-
if (edit.type === 'legend') {
|
|
540
|
-
expect(edit.offset.dx).toBe(50);
|
|
541
|
-
expect(edit.offset.dy).toBe(20);
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
chart.destroy();
|
|
545
|
-
});
|
|
546
|
-
|
|
547
|
-
it('click event after drag is suppressed (does not fire onLegendToggle)', () => {
|
|
548
|
-
const onLegendToggle = vi.fn();
|
|
549
|
-
const onEdit = vi.fn();
|
|
550
|
-
const chart = createChart(container, fullEditSpec, { onLegendToggle, onEdit });
|
|
551
|
-
|
|
552
|
-
const legendG = container.querySelector('.oc-legend') as SVGGElement | null;
|
|
553
|
-
if (!legendG) {
|
|
554
|
-
chart.destroy();
|
|
555
|
-
return;
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
// Drag the legend
|
|
559
|
-
simulateDrag(legendG, 200, 50, 250, 70);
|
|
560
|
-
|
|
561
|
-
// Fire a click after drag
|
|
562
|
-
legendG.dispatchEvent(createMouseEvent('click', 250, 70));
|
|
563
|
-
|
|
564
|
-
expect(onEdit).toHaveBeenCalledTimes(1);
|
|
565
|
-
// Click should have been suppressed by the drag handler
|
|
566
|
-
expect(onLegendToggle).not.toHaveBeenCalled();
|
|
567
|
-
|
|
568
|
-
chart.destroy();
|
|
569
|
-
});
|
|
570
|
-
});
|
|
571
|
-
|
|
572
|
-
// =========================================================================
|
|
573
|
-
// 6. wireConnectorEndpointDrag
|
|
574
|
-
// =========================================================================
|
|
575
|
-
describe('wireConnectorEndpointDrag', () => {
|
|
576
|
-
it('connector handles are created dynamically when onEdit is provided', () => {
|
|
577
|
-
const onEdit = vi.fn();
|
|
578
|
-
const chart = createChart(container, textAnnotatedSpec, { onEdit });
|
|
579
|
-
|
|
580
|
-
const annotationG = container.querySelector('.oc-annotation-text') as SVGGElement | null;
|
|
581
|
-
if (!annotationG) {
|
|
582
|
-
chart.destroy();
|
|
583
|
-
return;
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
// Check that connector handles exist within the annotation group
|
|
587
|
-
const handles = annotationG.querySelectorAll('.oc-connector-handle');
|
|
588
|
-
// There should be 2 handles (from and to) if a connector is present
|
|
589
|
-
const connector = annotationG.querySelector('.oc-annotation-connector');
|
|
590
|
-
if (connector) {
|
|
591
|
-
expect(handles.length).toBe(2);
|
|
592
|
-
// Verify they have the correct data-endpoint attributes
|
|
593
|
-
const endpoints = Array.from(handles).map((h) => h.getAttribute('data-endpoint'));
|
|
594
|
-
expect(endpoints).toContain('from');
|
|
595
|
-
expect(endpoints).toContain('to');
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
chart.destroy();
|
|
599
|
-
});
|
|
600
|
-
|
|
601
|
-
it('handles appear on mouseenter of annotation group', () => {
|
|
602
|
-
const onEdit = vi.fn();
|
|
603
|
-
const chart = createChart(container, textAnnotatedSpec, { onEdit });
|
|
604
|
-
|
|
605
|
-
const annotationG = container.querySelector('.oc-annotation-text') as SVGGElement | null;
|
|
606
|
-
if (!annotationG) {
|
|
607
|
-
chart.destroy();
|
|
608
|
-
return;
|
|
609
|
-
}
|
|
610
|
-
|
|
611
|
-
const handles = annotationG.querySelectorAll('.oc-connector-handle');
|
|
612
|
-
if (handles.length === 0) {
|
|
613
|
-
chart.destroy();
|
|
614
|
-
return;
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
// Initially handles should be invisible (opacity: 0)
|
|
618
|
-
for (const h of handles) {
|
|
619
|
-
expect(h.getAttribute('opacity')).toBe('0');
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
// Trigger mouseenter on the annotation group
|
|
623
|
-
annotationG.dispatchEvent(new MouseEvent('mouseenter', { bubbles: true }));
|
|
624
|
-
|
|
625
|
-
// Now handles should be visible
|
|
626
|
-
for (const h of handles) {
|
|
627
|
-
expect(h.getAttribute('opacity')).toBe('0.6');
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
chart.destroy();
|
|
631
|
-
});
|
|
632
|
-
|
|
633
|
-
it('handles disappear on mouseleave', () => {
|
|
634
|
-
const onEdit = vi.fn();
|
|
635
|
-
const chart = createChart(container, textAnnotatedSpec, { onEdit });
|
|
636
|
-
|
|
637
|
-
const annotationG = container.querySelector('.oc-annotation-text') as SVGGElement | null;
|
|
638
|
-
if (!annotationG) {
|
|
639
|
-
chart.destroy();
|
|
640
|
-
return;
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
const handles = annotationG.querySelectorAll('.oc-connector-handle');
|
|
644
|
-
if (handles.length === 0) {
|
|
645
|
-
chart.destroy();
|
|
646
|
-
return;
|
|
647
|
-
}
|
|
648
|
-
|
|
649
|
-
// Show handles first
|
|
650
|
-
annotationG.dispatchEvent(new MouseEvent('mouseenter', { bubbles: true }));
|
|
651
|
-
// Then hide them
|
|
652
|
-
annotationG.dispatchEvent(new MouseEvent('mouseleave', { bubbles: true }));
|
|
653
|
-
|
|
654
|
-
for (const h of handles) {
|
|
655
|
-
expect(h.getAttribute('opacity')).toBe('0');
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
chart.destroy();
|
|
659
|
-
});
|
|
660
|
-
|
|
661
|
-
it('dragging a handle fires onEdit with type annotation-connector and correct endpoint/offset', () => {
|
|
662
|
-
const onEdit = vi.fn();
|
|
663
|
-
const chart = createChart(container, textAnnotatedSpec, { onEdit });
|
|
664
|
-
|
|
665
|
-
const annotationG = container.querySelector('.oc-annotation-text') as SVGGElement | null;
|
|
666
|
-
if (!annotationG) {
|
|
667
|
-
chart.destroy();
|
|
668
|
-
return;
|
|
669
|
-
}
|
|
670
|
-
|
|
671
|
-
// Find the "from" handle
|
|
672
|
-
const fromHandle = annotationG.querySelector(
|
|
673
|
-
'.oc-connector-handle[data-endpoint="from"]',
|
|
674
|
-
) as SVGCircleElement | null;
|
|
675
|
-
|
|
676
|
-
if (!fromHandle) {
|
|
677
|
-
chart.destroy();
|
|
678
|
-
return;
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
simulateDrag(fromHandle, 100, 100, 130, 115);
|
|
682
|
-
|
|
683
|
-
expect(onEdit).toHaveBeenCalledTimes(1);
|
|
684
|
-
const edit: ElementEdit = onEdit.mock.calls[0][0];
|
|
685
|
-
expect(edit.type).toBe('annotation-connector');
|
|
686
|
-
if (edit.type === 'annotation-connector') {
|
|
687
|
-
expect(edit.endpoint).toBe('from');
|
|
688
|
-
expect(edit.annotation.type).toBe('text');
|
|
689
|
-
// No existing connectorOffset, so offset = delta (30, 15)
|
|
690
|
-
expect(edit.offset.dx).toBe(30);
|
|
691
|
-
expect(edit.offset.dy).toBe(15);
|
|
692
|
-
}
|
|
693
|
-
|
|
694
|
-
chart.destroy();
|
|
695
|
-
});
|
|
696
|
-
|
|
697
|
-
it('dragging the "to" handle fires onEdit with endpoint "to"', () => {
|
|
698
|
-
const onEdit = vi.fn();
|
|
699
|
-
const chart = createChart(container, textAnnotatedSpec, { onEdit });
|
|
700
|
-
|
|
701
|
-
const annotationG = container.querySelector('.oc-annotation-text') as SVGGElement | null;
|
|
702
|
-
if (!annotationG) {
|
|
703
|
-
chart.destroy();
|
|
704
|
-
return;
|
|
705
|
-
}
|
|
706
|
-
|
|
707
|
-
const toHandle = annotationG.querySelector(
|
|
708
|
-
'.oc-connector-handle[data-endpoint="to"]',
|
|
709
|
-
) as SVGCircleElement | null;
|
|
710
|
-
|
|
711
|
-
if (!toHandle) {
|
|
712
|
-
chart.destroy();
|
|
713
|
-
return;
|
|
714
|
-
}
|
|
715
|
-
|
|
716
|
-
simulateDrag(toHandle, 100, 100, 140, 125);
|
|
717
|
-
|
|
718
|
-
expect(onEdit).toHaveBeenCalledTimes(1);
|
|
719
|
-
const edit: ElementEdit = onEdit.mock.calls[0][0];
|
|
720
|
-
expect(edit.type).toBe('annotation-connector');
|
|
721
|
-
if (edit.type === 'annotation-connector') {
|
|
722
|
-
expect(edit.endpoint).toBe('to');
|
|
723
|
-
}
|
|
724
|
-
|
|
725
|
-
chart.destroy();
|
|
726
|
-
});
|
|
727
|
-
|
|
728
|
-
// The engine sorts annotations by zIndex, so the order the renderer draws them
|
|
729
|
-
// in is NOT the order they appear in the spec. Edit mode maps a dragged label
|
|
730
|
-
// back to its spec annotation, and if it maps by render position it edits the
|
|
731
|
-
// wrong one -- you drag one callout and a different one jumps.
|
|
732
|
-
describe('drags map to the right spec annotation, whatever the render order', () => {
|
|
733
|
-
it('drags the annotation you grabbed, not the one that shares its slot', () => {
|
|
734
|
-
const onEdit = vi.fn();
|
|
735
|
-
const chart = createChart(container, reorderedAnnotatedSpec, { onEdit });
|
|
736
|
-
|
|
737
|
-
// Grab by identity, not by position: this is the one the user clicked.
|
|
738
|
-
const secondG = container.querySelector(
|
|
739
|
-
'.oc-annotation-text[data-annotation-id="second"]',
|
|
740
|
-
) as SVGGElement;
|
|
741
|
-
expect(secondG).toBeTruthy();
|
|
742
|
-
|
|
743
|
-
simulateDrag(secondG, 100, 100, 120, 110);
|
|
744
|
-
|
|
745
|
-
expect(onEdit).toHaveBeenCalledTimes(1);
|
|
746
|
-
const edit: ElementEdit = onEdit.mock.calls[0][0];
|
|
747
|
-
expect(edit.type).toBe('annotation');
|
|
748
|
-
if (edit.type === 'annotation') {
|
|
749
|
-
// The edit must carry the annotation whose label was actually dragged.
|
|
750
|
-
// Pre-fix this returned the *other* one (zIndex had swapped their slots).
|
|
751
|
-
expect(edit.annotation.id).toBe('second');
|
|
752
|
-
}
|
|
753
|
-
|
|
754
|
-
chart.destroy();
|
|
755
|
-
});
|
|
756
|
-
|
|
757
|
-
it('each label carries the index of its own spec annotation', () => {
|
|
758
|
-
const chart = createChart(container, reorderedAnnotatedSpec, { onEdit: vi.fn() });
|
|
759
|
-
|
|
760
|
-
const groups = Array.from(
|
|
761
|
-
container.querySelectorAll('.oc-annotation-text'),
|
|
762
|
-
) as SVGGElement[];
|
|
763
|
-
expect(groups).toHaveLength(2);
|
|
764
|
-
|
|
765
|
-
for (const g of groups) {
|
|
766
|
-
const id = g.getAttribute('data-annotation-id');
|
|
767
|
-
const index = Number(g.getAttribute('data-annotation-index'));
|
|
768
|
-
const expected = reorderedAnnotatedSpec.annotations!.findIndex(
|
|
769
|
-
(a) => 'id' in a && a.id === id,
|
|
770
|
-
);
|
|
771
|
-
expect(index).toBe(expected);
|
|
772
|
-
}
|
|
773
|
-
|
|
774
|
-
chart.destroy();
|
|
775
|
-
});
|
|
776
|
-
});
|
|
777
|
-
|
|
778
|
-
// A curved connector has no <line> to read x2/y2 from, so edit mode recovers
|
|
779
|
-
// the tip by parsing the arrowhead polyline (`polyline.oc-annotation-arrowhead`,
|
|
780
|
-
// whose middle point is the tip). If that selector ever stops matching what the
|
|
781
|
-
// renderer emits, `querySelector` returns null, the tip silently falls back to
|
|
782
|
-
// (0, 0), and the "to" handle jumps to the SVG origin. Nothing else in this file
|
|
783
|
-
// renders an arrowed connector, so these are the only tests that can catch it.
|
|
784
|
-
describe('curved connector (the arrowhead-tip path)', () => {
|
|
785
|
-
it('renders exactly one arrowhead polyline that edit mode can find', () => {
|
|
786
|
-
const chart = createChart(container, curvedAnnotatedSpec, { onEdit: vi.fn() });
|
|
787
|
-
|
|
788
|
-
const annotationG = container.querySelector('.oc-annotation-text') as SVGGElement;
|
|
789
|
-
expect(annotationG).toBeTruthy();
|
|
790
|
-
// The path the renderer draws...
|
|
791
|
-
expect(annotationG.querySelector('path.oc-annotation-connector')).toBeTruthy();
|
|
792
|
-
// ...and the exact selector the drag code uses to find its tip.
|
|
793
|
-
const arrowheads = annotationG.querySelectorAll('polyline.oc-annotation-arrowhead');
|
|
794
|
-
expect(arrowheads).toHaveLength(1);
|
|
795
|
-
|
|
796
|
-
chart.destroy();
|
|
797
|
-
});
|
|
798
|
-
|
|
799
|
-
it('puts the "to" handle on the arrowhead tip, not at the origin', () => {
|
|
800
|
-
const chart = createChart(container, curvedAnnotatedSpec, { onEdit: vi.fn() });
|
|
801
|
-
|
|
802
|
-
const annotationG = container.querySelector('.oc-annotation-text') as SVGGElement;
|
|
803
|
-
const arrowhead = annotationG.querySelector(
|
|
804
|
-
'polyline.oc-annotation-arrowhead',
|
|
805
|
-
) as SVGPolylineElement;
|
|
806
|
-
const toHandle = annotationG.querySelector(
|
|
807
|
-
'.oc-connector-handle[data-endpoint="to"]',
|
|
808
|
-
) as SVGCircleElement;
|
|
809
|
-
expect(toHandle).toBeTruthy();
|
|
810
|
-
|
|
811
|
-
// "baseLeft tip baseRight" -- the tip is the middle point.
|
|
812
|
-
const [tipX, tipY] = (arrowhead.getAttribute('points')?.split(' ')[1] ?? '').split(',');
|
|
813
|
-
expect(Number(tipX)).toBeGreaterThan(0);
|
|
814
|
-
|
|
815
|
-
expect(Number(toHandle.getAttribute('cx'))).toBeCloseTo(Number(tipX), 3);
|
|
816
|
-
expect(Number(toHandle.getAttribute('cy'))).toBeCloseTo(Number(tipY), 3);
|
|
817
|
-
|
|
818
|
-
chart.destroy();
|
|
819
|
-
});
|
|
820
|
-
|
|
821
|
-
it('dragging the "to" handle of a curved connector fires a real edit', () => {
|
|
822
|
-
const onEdit = vi.fn();
|
|
823
|
-
const chart = createChart(container, curvedAnnotatedSpec, { onEdit });
|
|
824
|
-
|
|
825
|
-
const annotationG = container.querySelector('.oc-annotation-text') as SVGGElement;
|
|
826
|
-
const toHandle = annotationG.querySelector(
|
|
827
|
-
'.oc-connector-handle[data-endpoint="to"]',
|
|
828
|
-
) as SVGCircleElement;
|
|
829
|
-
expect(toHandle).toBeTruthy();
|
|
830
|
-
|
|
831
|
-
simulateDrag(toHandle, 100, 100, 130, 115);
|
|
832
|
-
|
|
833
|
-
expect(onEdit).toHaveBeenCalledTimes(1);
|
|
834
|
-
const edit: ElementEdit = onEdit.mock.calls[0][0];
|
|
835
|
-
expect(edit.type).toBe('annotation-connector');
|
|
836
|
-
if (edit.type === 'annotation-connector') {
|
|
837
|
-
expect(edit.endpoint).toBe('to');
|
|
838
|
-
expect(edit.offset.dx).toBe(30);
|
|
839
|
-
expect(edit.offset.dy).toBe(15);
|
|
840
|
-
}
|
|
841
|
-
|
|
842
|
-
chart.destroy();
|
|
843
|
-
});
|
|
844
|
-
});
|
|
845
|
-
});
|
|
846
|
-
|
|
847
|
-
// =========================================================================
|
|
848
|
-
// 7. wireSeriesLabelDrag
|
|
849
|
-
// =========================================================================
|
|
850
|
-
describe('wireSeriesLabelDrag', () => {
|
|
851
|
-
it('series labels get cursor:grab when onEdit is provided', () => {
|
|
852
|
-
const onEdit = vi.fn();
|
|
853
|
-
const chart = createChart(container, fullEditSpec, { onEdit });
|
|
854
|
-
|
|
855
|
-
const seriesLabels = container.querySelectorAll('.oc-mark-label[data-series]');
|
|
856
|
-
if (seriesLabels.length === 0) {
|
|
857
|
-
// Labels may not render in the test env depending on layout
|
|
858
|
-
chart.destroy();
|
|
859
|
-
return;
|
|
860
|
-
}
|
|
861
|
-
|
|
862
|
-
for (const el of seriesLabels) {
|
|
863
|
-
expect((el as HTMLElement).style.cursor).toBe('grab');
|
|
864
|
-
}
|
|
865
|
-
|
|
866
|
-
chart.destroy();
|
|
867
|
-
});
|
|
868
|
-
|
|
869
|
-
it('dragging fires onEdit with type series-label, correct series name, and accumulated offset', () => {
|
|
870
|
-
const onEdit = vi.fn();
|
|
871
|
-
const chart = createChart(container, fullEditSpec, { onEdit });
|
|
872
|
-
|
|
873
|
-
const seriesLabel = container.querySelector(
|
|
874
|
-
'.oc-mark-label[data-series]',
|
|
875
|
-
) as SVGTextElement | null;
|
|
876
|
-
if (!seriesLabel) {
|
|
877
|
-
chart.destroy();
|
|
878
|
-
return;
|
|
879
|
-
}
|
|
880
|
-
|
|
881
|
-
const seriesName = seriesLabel.getAttribute('data-series')!;
|
|
882
|
-
simulateDrag(seriesLabel, 100, 100, 135, 118);
|
|
883
|
-
|
|
884
|
-
// Find the onEdit call for a series-label (could have other edits from other elements)
|
|
885
|
-
const seriesLabelCall = onEdit.mock.calls.find(
|
|
886
|
-
(call: [ElementEdit]) => call[0].type === 'series-label',
|
|
887
|
-
);
|
|
888
|
-
expect(seriesLabelCall).toBeDefined();
|
|
889
|
-
|
|
890
|
-
const edit: ElementEdit = seriesLabelCall![0];
|
|
891
|
-
expect(edit.type).toBe('series-label');
|
|
892
|
-
if (edit.type === 'series-label') {
|
|
893
|
-
expect(edit.series).toBe(seriesName);
|
|
894
|
-
// No existing offset, so offset = delta (35, 18)
|
|
895
|
-
expect(edit.offset.dx).toBe(35);
|
|
896
|
-
expect(edit.offset.dy).toBe(18);
|
|
897
|
-
}
|
|
898
|
-
|
|
899
|
-
chart.destroy();
|
|
900
|
-
});
|
|
901
|
-
});
|
|
902
|
-
});
|