@mhkeller/layercake-annotations 0.1.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.
Files changed (42) hide show
  1. package/README.md +184 -0
  2. package/dist/Annotations.svelte +15 -0
  3. package/dist/Annotations.svelte.d.ts +14 -0
  4. package/dist/Editor.svelte +213 -0
  5. package/dist/Editor.svelte.d.ts +19 -0
  6. package/dist/README.md +200 -0
  7. package/dist/Static.svelte +24 -0
  8. package/dist/Static.svelte.d.ts +12 -0
  9. package/dist/components/AnnotationEditor.svelte +127 -0
  10. package/dist/components/AnnotationEditor.svelte.d.ts +14 -0
  11. package/dist/components/AnnotationsData.svelte +56 -0
  12. package/dist/components/AnnotationsData.svelte.d.ts +15 -0
  13. package/dist/components/ArrowZone.svelte +366 -0
  14. package/dist/components/ArrowZone.svelte.d.ts +27 -0
  15. package/dist/components/ArrowheadMarker.svelte +18 -0
  16. package/dist/components/ArrowheadMarker.svelte.d.ts +14 -0
  17. package/dist/components/Arrows.svelte +175 -0
  18. package/dist/components/Arrows.svelte.d.ts +20 -0
  19. package/dist/components/Draggable.svelte +121 -0
  20. package/dist/components/Draggable.svelte.d.ts +33 -0
  21. package/dist/components/EditableText.svelte +131 -0
  22. package/dist/components/EditableText.svelte.d.ts +16 -0
  23. package/dist/components/ResizeHandles.svelte +146 -0
  24. package/dist/components/ResizeHandles.svelte.d.ts +21 -0
  25. package/dist/index.d.ts +7 -0
  26. package/dist/index.js +11 -0
  27. package/dist/modules/arrowUtils.d.ts +17 -0
  28. package/dist/modules/arrowUtils.js +32 -0
  29. package/dist/modules/coordinates.d.ts +85 -0
  30. package/dist/modules/coordinates.js +139 -0
  31. package/dist/modules/createRef.svelte.d.ts +7 -0
  32. package/dist/modules/createRef.svelte.js +10 -0
  33. package/dist/modules/filterObject.d.ts +7 -0
  34. package/dist/modules/filterObject.js +14 -0
  35. package/dist/modules/invertScale.d.ts +1 -0
  36. package/dist/modules/invertScale.js +5 -0
  37. package/dist/modules/newAnnotation.d.ts +9 -0
  38. package/dist/modules/newAnnotation.js +26 -0
  39. package/dist/modules/ordinalInvert.d.ts +9 -0
  40. package/dist/modules/ordinalInvert.js +22 -0
  41. package/dist/types.d.ts +162 -0
  42. package/package.json +61 -0
package/README.md ADDED
@@ -0,0 +1,184 @@
1
+ # LayerCake Annotations
2
+
3
+ Add interactive text annotations with swoopy arrows to [LayerCake](https://layercake.graphics) charts.
4
+
5
+ ## Installation
6
+
7
+ ```sh
8
+ pnpm add @mhkeller/layercake-annotations
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ```svelte
14
+ <script>
15
+ import { LayerCake } from 'layercake';
16
+ import { Annotations } from '@mhkeller/layercake-annotations';
17
+
18
+ let annotations = $state([]);
19
+ </script>
20
+
21
+ <LayerCake data={data} x="date" y="value">
22
+ <Annotations bind:annotations />
23
+ </LayerCake>
24
+ ```
25
+
26
+ **Creating annotations:**
27
+ - Click anywhere on the chart to create
28
+ - Drag to reposition
29
+ - Double-click text to edit
30
+ - Hover + Delete/Backspace to remove
31
+
32
+ **Editing text:**
33
+ - Double-click to edit
34
+ - Enter to save
35
+ - Shift+Enter for line breaks
36
+ - Escape to cancel
37
+
38
+ **Formatting:**
39
+ - Cmd+click annotation to cycle text alignment: left → center → right
40
+
41
+ **Creating arrows:**
42
+ - Hover over annotation to reveal handles on west/east edges
43
+ - Drag a handle outward to create an arrow
44
+ - Cmd+click arrow to cycle through: curved clockwise → straight → curved counter-clockwise
45
+
46
+ ## Props
47
+
48
+ | Prop | Type | Default | Description |
49
+ |------|------|---------|-------------|
50
+ | `annotations` | `Annotation[]` | `[]` | Array of annotation objects (bindable) |
51
+ | `editable` | `boolean` | `true` | Enable editing. Set `false` for read-only display |
52
+
53
+ ## Annotation Data Structure
54
+
55
+ ```js
56
+ {
57
+ id: 0, // Unique identifier
58
+ date: new Date('2024-03-15'), // X value (matches LayerCake x accessor)
59
+ value: 42, // Y value (matches LayerCake y accessor)
60
+ dx: 5, // X offset: percentage of chart width (-100 to 100)
61
+ dy: -10, // Y offset: percentage of chart height (-100 to 100)
62
+ text: 'Peak value', // Annotation text (supports line breaks)
63
+ width: '120px', // Optional: fixed width
64
+ align: 'left', // Optional: 'left', 'center', or 'right'
65
+ arrows: [] // Array of arrows (see below)
66
+ }
67
+ ```
68
+
69
+ ### Offsets explained
70
+
71
+ The `dx` and `dy` values are **percentages of the chart dimensions** (not decimals 0–1).
72
+
73
+ | Value | Meaning |
74
+ |-------|---------|
75
+ | `dx: 0` | Annotation left edge aligned with data point |
76
+ | `dx: 10` | Shifted right by 10% of chart width |
77
+ | `dx: -5` | Shifted left by 5% of chart width |
78
+ | `dy: -15` | Shifted up by 15% of chart height |
79
+
80
+ ### Arrow structure
81
+
82
+ ```js
83
+ {
84
+ side: 'east', // 'west' or 'east' - which side of annotation
85
+ clockwise: true, // true = clockwise curve, false = counter-clockwise, null = straight
86
+ source: {
87
+ dx: 12, // Pixels from annotation edge (horizontal)
88
+ dy: 15 // Pixels from annotation center (vertical)
89
+ },
90
+ target: {
91
+ date: new Date('2024-03-15'), // X data value (matches LayerCake x accessor)
92
+ value: 42, // Y data value
93
+ dx: 0, // % offset for ordinal X scales (0-100)
94
+ dy: 0 // % offset for ordinal Y scales (0-100)
95
+ }
96
+ }
97
+ ```
98
+
99
+ ## Full Example
100
+
101
+ ```svelte
102
+ <script>
103
+ import { LayerCake, Svg, Html } from 'layercake';
104
+ import { Annotations } from '@mhkeller/layercake-annotations';
105
+ import Line from './Line.svelte';
106
+
107
+ let data = [...];
108
+ let annotations = $state([
109
+ {
110
+ id: 0,
111
+ date: new Date('2024-06-01'),
112
+ value: 150,
113
+ dx: 2,
114
+ dy: -8,
115
+ text: 'Summer peak',
116
+ width: '100px',
117
+ arrows: [{
118
+ side: 'west',
119
+ clockwise: false,
120
+ source: { dx: -12, dy: 0 },
121
+ target: { date: new Date('2024-06-01'), value: 150, dx: 0, dy: 0 }
122
+ }]
123
+ }
124
+ ]);
125
+
126
+ let editable = $state(true);
127
+ </script>
128
+
129
+ <label>
130
+ <input type="checkbox" bind:checked={editable} /> Edit mode
131
+ </label>
132
+
133
+ <div class="chart-container">
134
+ <LayerCake {data} x="date" y="value">
135
+ <Svg><Line /></Svg>
136
+ <Annotations bind:annotations {editable} />
137
+ </LayerCake>
138
+ </div>
139
+ ```
140
+
141
+ ## TypeScript
142
+
143
+ ```ts
144
+ import type { Annotation, Arrow } from '@mhkeller/layercake-annotations/types';
145
+ ```
146
+
147
+ ## Components
148
+
149
+ | Export | Description |
150
+ |--------|-------------|
151
+ | `Annotations` | Main component – set `editable` prop to toggle modes |
152
+ | `AnnotationsEditor` | Edit mode only |
153
+ | `AnnotationsStatic` | Read-only mode only |
154
+
155
+ ## Development
156
+
157
+ ```sh
158
+ pnpm install
159
+ pnpm dev # Start dev server at localhost:5173
160
+ pnpm test # Run Playwright visual regression tests
161
+ pnpm package # Build for npm distribution
162
+ ```
163
+
164
+ ## Architecture
165
+
166
+ ```
167
+ src/lib/
168
+ ├── Annotations.svelte # Wrapper: switches Editor/Static based on editable
169
+ ├── Editor.svelte # Edit mode: state management, context providers
170
+ ├── Static.svelte # Read-only: renders annotations + arrows
171
+ ├── components/
172
+ │ ├── AnnotationEditor # Draggable annotation with text editing
173
+ │ ├── AnnotationsData # Static annotation renderer
174
+ │ ├── ArrowZone # Handles for creating/editing arrows
175
+ │ ├── Arrows # SVG arrow path rendering
176
+ │ ├── Draggable # Drag behavior wrapper
177
+ │ ├── EditableText # Contenteditable text input
178
+ │ └── ResizeHandles # Width resize handles
179
+ ├── modules/
180
+ │ ├── coordinates.js # Position calculations
181
+ │ ├── invertScale.js # Pixel → data value conversion
182
+ │ └── arrowUtils.js # SVG arc path generation
183
+ └── types.d.ts # TypeScript definitions
184
+ ```
@@ -0,0 +1,15 @@
1
+ <script>
2
+ /** @typedef {import('./types.js').Annotation} Annotation */
3
+
4
+ import Editor from './Editor.svelte';
5
+ import Static from './Static.svelte';
6
+
7
+ /** @type {{ annotations?: Annotation[], editable?: boolean }} */
8
+ let { annotations = $bindable([]), editable = true } = $props();
9
+ </script>
10
+
11
+ {#if editable}
12
+ <Editor bind:annotations />
13
+ {:else}
14
+ <Static {annotations} />
15
+ {/if}
@@ -0,0 +1,14 @@
1
+ export default Annotations;
2
+ export type Annotation = import("./types.js").Annotation;
3
+ type Annotations = {
4
+ $on?(type: string, callback: (e: any) => void): () => void;
5
+ $set?(props: Partial<$$ComponentProps>): void;
6
+ };
7
+ declare const Annotations: import("svelte").Component<{
8
+ annotations?: Annotation[];
9
+ editable?: boolean;
10
+ }, {}, "annotations">;
11
+ type $$ComponentProps = {
12
+ annotations?: Annotation[];
13
+ editable?: boolean;
14
+ };
@@ -0,0 +1,213 @@
1
+ <script>
2
+ /** @typedef {import('./types.js').Annotation} Annotation */
3
+ /** @typedef {import('./types.js').Arrow} Arrow */
4
+ /** @typedef {import('./types.js').HoverState} HoverState */
5
+ /** @typedef {import('./types.js').DragState} DragState */
6
+ /** @typedef {import('./types.js').SaveAnnotationConfigFn} SaveAnnotationConfigFn */
7
+ /**
8
+ * @template T
9
+ * @typedef {import('./types.js').Ref<T>} Ref
10
+ */
11
+
12
+ import { getContext, setContext } from 'svelte';
13
+ import { Svg, Html } from 'layercake';
14
+ import { debounce } from 'underscore';
15
+
16
+ import AnnotationEditor from './components/AnnotationEditor.svelte';
17
+ import ArrowheadMarker from './components/ArrowheadMarker.svelte';
18
+ import Arrows from './components/Arrows.svelte';
19
+
20
+ import createRef from './modules/createRef.svelte.js';
21
+ import newAnnotation from './modules/newAnnotation.js';
22
+
23
+ /** @type {{ annotations?: Annotation[], containerClass?: string }} */
24
+ let { annotations: annos = $bindable([]), containerClass } = $props();
25
+
26
+ /**
27
+ * LayerCake context
28
+ */
29
+ const { xScale, yScale, config } = getContext('LayerCake');
30
+
31
+ /** @type {SaveAnnotationConfigFn | undefined} */
32
+ const saveAnnotationConfig = getContext('saveAnnotationConfig');
33
+
34
+ /**
35
+ * Save the config and log it for easy copy-paste
36
+ */
37
+ const saveConfig_debounced = debounce((annos) => {
38
+ console.log('Annotations config:', JSON.stringify(annos, null, 2));
39
+ if (saveAnnotationConfig) {
40
+ saveAnnotationConfig(annos);
41
+ }
42
+ }, 1_000);
43
+
44
+ /**
45
+ * State vars
46
+ */
47
+ let idCounter = Math.max(...annos.map((d) => d.id), -1);
48
+ let annotations = $state(annos);
49
+
50
+ /** @type {Ref<boolean>} */
51
+ const isEditing = createRef(false);
52
+
53
+ /** @type {Ref<HoverState | null>} */
54
+ const hovering = createRef(null);
55
+
56
+ /** @type {Ref<boolean>} */
57
+ const moving = createRef(false);
58
+
59
+ /** @type {Ref<DragState | null>} - Preview arrow shown during drag */
60
+ const previewArrow = createRef(null);
61
+
62
+ setContext('isEditing', isEditing);
63
+ setContext('hovering', hovering);
64
+ setContext('moving', moving);
65
+ setContext('previewArrow', previewArrow);
66
+
67
+ /**
68
+ * Add a new annotation to the chart
69
+ */
70
+ function onclick(e) {
71
+ if (isEditing.value === true) return;
72
+
73
+ const annotation = newAnnotation(e, ++idCounter, {
74
+ xScale: $xScale,
75
+ yScale: $yScale,
76
+ config: $config
77
+ });
78
+ annotations.push(annotation);
79
+ saveConfig_debounced(annotations);
80
+ }
81
+
82
+ /**
83
+ * Delete an annotation from the chart
84
+ */
85
+ async function deleteAnnotation(id) {
86
+ annotations = annotations.filter((d) => d.id !== id);
87
+ saveConfig_debounced(annotations);
88
+ }
89
+
90
+ /**
91
+ * Modify the annotation's coordinates on drag
92
+ */
93
+ function modifyAnnotation(id, newProps) {
94
+ annotations.forEach((d, i) => {
95
+ if (d.id === id) {
96
+ annotations[i] = {
97
+ ...d,
98
+ ...newProps
99
+ };
100
+ }
101
+ });
102
+ saveConfig_debounced(annotations);
103
+ }
104
+
105
+ /**
106
+ * Set or update an arrow on an annotation
107
+ * Arrow structure: { side, clockwise, source: { dx, dy }, target: { [xKey], [yKey] } }
108
+ */
109
+ function setArrow(id, arrow) {
110
+ const annotation = annotations.find((d) => d.id === id);
111
+ if (!annotation) return;
112
+
113
+ const existingIndex = annotation.arrows.findIndex((a) => a.side === arrow.side);
114
+
115
+ if (existingIndex >= 0) {
116
+ annotation.arrows[existingIndex] = arrow;
117
+ } else {
118
+ annotation.arrows.push(arrow);
119
+ }
120
+
121
+ saveConfig_debounced(annotations);
122
+ }
123
+
124
+ /**
125
+ * Modify an arrow's properties (e.g., clockwise)
126
+ */
127
+ function modifyArrow(id, side, attrs) {
128
+ const annotation = annotations.find((d) => d.id === id);
129
+ if (!annotation) return;
130
+
131
+ const arrow = annotation.arrows.find((a) => a.side === side);
132
+ if (!arrow) return;
133
+
134
+ Object.assign(arrow, attrs);
135
+ saveConfig_debounced(annotations);
136
+ }
137
+
138
+ /**
139
+ * Delete an arrow from an annotation
140
+ */
141
+ function deleteArrow(id, side) {
142
+ const annotation = annotations.find((d) => d.id === id);
143
+ if (!annotation) return;
144
+
145
+ const len = annotation.arrows.length;
146
+ annotation.arrows = annotation.arrows.filter((a) => a.side !== side);
147
+
148
+ // If we were hovering over an empty arrow zone, delete the annotation
149
+ if (len === annotation.arrows.length) {
150
+ deleteAnnotation(annotation.id);
151
+ }
152
+ saveConfig_debounced(annotations);
153
+ }
154
+
155
+ /**
156
+ * If we press the delete key while hovering, delete the annotation or arrow
157
+ */
158
+ function onkeydown(e) {
159
+ const hover = hovering.value;
160
+ if (!hover || isEditing.value === true) return;
161
+
162
+ if (e.key === 'Delete' || e.key === 'Backspace') {
163
+ if (hover.type === 'body') {
164
+ deleteAnnotation(hover.annotationId);
165
+ } else if (hover.type === 'arrow' && hover.side) {
166
+ deleteArrow(hover.annotationId, hover.side);
167
+ }
168
+ saveConfig_debounced(annotations);
169
+ }
170
+ }
171
+
172
+ /**
173
+ * Save our modifier functions to the context
174
+ */
175
+ setContext('modifyAnnotation', modifyAnnotation);
176
+ setContext('setArrow', setArrow);
177
+ setContext('modifyArrow', modifyArrow);
178
+ </script>
179
+
180
+ {#snippet defs()}
181
+ <ArrowheadMarker />
182
+ {/snippet}
183
+
184
+ <Svg {defs}>
185
+ <Arrows {annotations} />
186
+ </Svg>
187
+
188
+ <Html>
189
+ <div
190
+ onclick={debounce(onclick, 250, true)}
191
+ onkeydown={(e) => e.key === 'Enter' && onclick(e)}
192
+ role="button"
193
+ tabindex="0"
194
+ aria-label="Click to add annotation"
195
+ class="note-listener"
196
+ ></div>
197
+
198
+ <div class="layercake-annotations">
199
+ {#each annotations as d (d.id)}
200
+ <AnnotationEditor {d} {containerClass} />
201
+ {/each}
202
+ </div>
203
+ </Html>
204
+
205
+ <svelte:window {onkeydown} />
206
+
207
+ <style>
208
+ .note-listener {
209
+ width: 100%;
210
+ height: 100%;
211
+ cursor: copy;
212
+ }
213
+ </style>
@@ -0,0 +1,19 @@
1
+ export default Editor;
2
+ export type Annotation = import("./types.js").Annotation;
3
+ export type Arrow = import("./types.js").Arrow;
4
+ export type HoverState = import("./types.js").HoverState;
5
+ export type DragState = import("./types.js").DragState;
6
+ export type SaveAnnotationConfigFn = import("./types.js").SaveAnnotationConfigFn;
7
+ export type Ref<T> = import("./types.js").Ref<T>;
8
+ type Editor = {
9
+ $on?(type: string, callback: (e: any) => void): () => void;
10
+ $set?(props: Partial<$$ComponentProps>): void;
11
+ };
12
+ declare const Editor: import("svelte").Component<{
13
+ annotations?: Annotation[];
14
+ containerClass?: string;
15
+ }, {}, "annotations">;
16
+ type $$ComponentProps = {
17
+ annotations?: Annotation[];
18
+ containerClass?: string;
19
+ };
package/dist/README.md ADDED
@@ -0,0 +1,200 @@
1
+ # LayerCake Annotations - Internal Architecture
2
+
3
+ This document describes the internal architecture of the annotation library for contributors and maintainers.
4
+
5
+ ## Overview
6
+
7
+ LayerCake Annotations provides interactive text annotations with swoopy arrows for [LayerCake](https://layercake.graphics/) charts. It supports both linear and ordinal scales.
8
+
9
+ ## Component Structure
10
+
11
+ ```
12
+ src/lib/
13
+ ├── Editor.svelte # Interactive editing mode (exported as AnnotationsEditor)
14
+ ├── Static.svelte # Read-only display mode (exported as AnnotationsStatic)
15
+ ├── components/
16
+ │ ├── AnnotationEditor.svelte # Individual annotation wrapper
17
+ │ ├── ArrowZone.svelte # Draggable arrow handles
18
+ │ ├── Arrows.svelte # SVG arrow rendering
19
+ │ ├── Draggable.svelte # Generic drag behavior
20
+ │ ├── EditableText.svelte # Editable text input
21
+ │ ├── ResizeHandles.svelte # Width resize handles
22
+ │ ├── ArrowheadMarker.svelte # SVG arrowhead definition
23
+ │ └── AnnotationsData.svelte # Static annotations renderer
24
+ └── modules/
25
+ ├── coordinates.js # Shared coordinate calculations
26
+ ├── arrowUtils.js # SVG path generation
27
+ ├── createRef.svelte.js # Reactive state refs for context
28
+ ├── invertScale.js # Scale inversion utilities
29
+ ├── ordinalInvert.js # Ordinal scale inversion
30
+ ├── filterObject.js # Object utilities
31
+ └── newAnnotation.js # Annotation factory
32
+ ```
33
+
34
+ ## Data Model
35
+
36
+ ### Annotation Object
37
+
38
+ ```typescript
39
+ {
40
+ id: number, // Unique identifier
41
+ [xKey]: any, // X data value (key from LayerCake config)
42
+ [yKey]: any, // Y data value (key from LayerCake config)
43
+ dx: number, // X offset: -100 to 100 (percentage of chart width)
44
+ dy: number, // Y offset: -100 to 100 (percentage of chart height)
45
+ text: string, // Annotation text (supports line breaks)
46
+ width?: string, // Box width (e.g., "150px")
47
+ align?: string, // Text alignment: 'left', 'center', or 'right'
48
+ arrows: Arrow[] // Attached arrows
49
+ }
50
+ ```
51
+
52
+ **Note**: `dx` and `dy` are percentages (not decimals). Use `dx: 10` for 10% right, `dx: -5` for 5% left.
53
+
54
+ ### Arrow Object
55
+
56
+ ```typescript
57
+ {
58
+ side: 'west' | 'east', // Which side of annotation the arrow originates from
59
+ clockwise: boolean|null, // true = clockwise arc, false = counter-clockwise, null = straight
60
+ source: {
61
+ dx: number, // Pixels from annotation edge (neg = further out)
62
+ dy: number // Pixels from annotation vertical center
63
+ },
64
+ target: {
65
+ [xKey]: any, // X data value (same accessor as LayerCake)
66
+ [yKey]: any, // Y data value
67
+ dx?: number, // 0-100: % offset within ordinal band (X)
68
+ dy?: number // 0-100: % offset within ordinal band (Y)
69
+ }
70
+ }
71
+ ```
72
+
73
+ ## Coordinate System
74
+
75
+ The library uses multiple coordinate systems:
76
+
77
+ ### Annotation Position
78
+
79
+ ```
80
+ Final position = scale(dataValue) + (dx/100 × chartDimension)
81
+ ```
82
+
83
+ | Property | Unit | Range | Example |
84
+ |----------|------|-------|---------|
85
+ | `[xKey]` | Data value | Depends on data | `new Date('2024-01-15')` |
86
+ | `[yKey]` | Data value | Depends on data | `42` |
87
+ | `dx` | % of chart width | -100 to 100 | `5` = 5% right |
88
+ | `dy` | % of chart height | -100 to 100 | `-10` = 10% up |
89
+
90
+ ### Arrow Source Position
91
+
92
+ Pixels relative to annotation box edge:
93
+
94
+ | Property | Unit | Meaning |
95
+ |----------|------|---------|
96
+ | `source.dx` | Pixels | Distance from annotation edge (west: left edge, east: right edge) |
97
+ | `source.dy` | Pixels | Distance from annotation vertical center |
98
+
99
+ ### Arrow Target Position
100
+
101
+ | Property | Unit | When used |
102
+ |----------|------|-----------|
103
+ | `[xKey]`, `[yKey]` | Data values | Always (same keys as LayerCake config) |
104
+ | `dx`, `dy` | % within band (0-100) | Only for ordinal scales |
105
+
106
+ ## Keyboard Shortcuts
107
+
108
+ | Shortcut | Action |
109
+ |----------|--------|
110
+ | Double-click | Edit annotation text |
111
+ | Enter | Save text and exit edit mode |
112
+ | Shift+Enter | Insert line break while editing |
113
+ | Escape | Cancel editing |
114
+ | Cmd+click | Cycle text alignment: left → center → right |
115
+ | Delete/Backspace | Delete hovered annotation or arrow |
116
+ | Cmd+click on arrow | Toggle curve direction: clockwise → straight → counter-clockwise |
117
+
118
+ ## State Management
119
+
120
+ State is shared via Svelte context using the `createRef` pattern:
121
+
122
+ ```javascript
123
+ // In Editor.svelte
124
+ const hovering = createRef(null);
125
+ setContext('hovering', hovering);
126
+
127
+ // In child components
128
+ const hovering = getContext('hovering');
129
+ hovering.value = { annotationId: 0, type: 'body' };
130
+ ```
131
+
132
+ ### Context Values
133
+
134
+ | Key | Type | Description |
135
+ |-----|------|-------------|
136
+ | `hovering` | `HoverState \| null` | Currently hovered element |
137
+ | `moving` | `boolean` | Whether dragging is in progress |
138
+ | `isEditing` | `boolean` | Whether text is being edited |
139
+ | `previewArrow` | `DragState \| null` | Live arrow during drag |
140
+ | `modifyAnnotation` | `function` | Update annotation props |
141
+ | `setArrow` | `function` | Create/update arrow |
142
+ | `modifyArrow` | `function` | Modify arrow properties |
143
+
144
+ ## Key Modules
145
+
146
+ ### coordinates.js
147
+
148
+ Centralizes all coordinate calculations to prevent drift:
149
+
150
+ ```javascript
151
+ import {
152
+ getAnnotationBox, // Get annotation position/size
153
+ getArrowSource, // Get arrow source in pixels
154
+ getArrowTarget, // Get arrow target in pixels
155
+ calculateSourceDx, // Convert pixel to stored offset
156
+ DEFAULT_ANNOTATION_WIDTH
157
+ } from './modules/coordinates.js';
158
+ ```
159
+
160
+ ### arrowUtils.js
161
+
162
+ Generates SVG arc paths:
163
+
164
+ ```javascript
165
+ import { createArrowPath } from './modules/arrowUtils.js';
166
+
167
+ const path = createArrowPath(
168
+ { x: 100, y: 50 }, // source
169
+ { x: 200, y: 100 }, // target
170
+ true, // clockwise
171
+ Math.PI / 2 // angle (optional)
172
+ );
173
+ ```
174
+
175
+ ## Testing
176
+
177
+ Tests use Playwright for visual regression testing:
178
+
179
+ ```bash
180
+ pnpm test # Run tests
181
+ pnpm exec playwright test --update-snapshots # Update snapshots
182
+ ```
183
+
184
+ Tests cover both linear scale (`/`) and ordinal scale (`/ordinal`) examples.
185
+
186
+ ## Common Tasks
187
+
188
+ ### Adding a new arrow property
189
+
190
+ 1. Update `Arrow` type in `types.d.ts`
191
+ 2. Update `setArrow` in `Editor.svelte` to handle the property
192
+ 3. Update `ArrowZone.svelte` to read/write the property
193
+ 4. Update `Arrows.svelte` if it affects rendering
194
+
195
+ ### Debugging coordinate issues
196
+
197
+ 1. Check that `coordinates.js` functions are being used consistently
198
+ 2. Verify the correct `scales` object is being passed
199
+ 3. For east arrows, remember `dx` is from the RIGHT edge
200
+
@@ -0,0 +1,24 @@
1
+ <script>
2
+ /** @typedef {import('./types.js').Annotation} Annotation */
3
+
4
+ import { Svg, Html } from 'layercake';
5
+
6
+ import AnnotationsData from './components/AnnotationsData.svelte';
7
+ import ArrowheadMarker from './components/ArrowheadMarker.svelte';
8
+ import Arrows from './components/Arrows.svelte';
9
+
10
+ /** @type {{ annotations?: Annotation[] }} */
11
+ let { annotations = [] } = $props();
12
+ </script>
13
+
14
+ {#snippet defs()}
15
+ <ArrowheadMarker />
16
+ {/snippet}
17
+
18
+ <Svg {defs}>
19
+ <Arrows {annotations} />
20
+ </Svg>
21
+
22
+ <Html>
23
+ <AnnotationsData {annotations} />
24
+ </Html>
@@ -0,0 +1,12 @@
1
+ export default Static;
2
+ export type Annotation = import("./types.js").Annotation;
3
+ type Static = {
4
+ $on?(type: string, callback: (e: any) => void): () => void;
5
+ $set?(props: Partial<$$ComponentProps>): void;
6
+ };
7
+ declare const Static: import("svelte").Component<{
8
+ annotations?: Annotation[];
9
+ }, {}, "">;
10
+ type $$ComponentProps = {
11
+ annotations?: Annotation[];
12
+ };