@wicklib/layouts 0.0.0-stage → 0.13.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/LICENSE +202 -0
- package/README.md +136 -2
- package/dist/index.cjs +438 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +142 -0
- package/dist/index.d.ts +142 -0
- package/dist/index.js +436 -0
- package/dist/index.js.map +1 -0
- package/package.json +53 -3
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { ChartOptions, ChartApi, Unsubscribe } from '@wicklib/core';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Free multi-chart layout primitives for Wicklib.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Crosshair synchronization mode.
|
|
9
|
+
*
|
|
10
|
+
* `time` mirrors the vertical time line. `full` also mirrors a horizontal line at the same
|
|
11
|
+
* relative pane height.
|
|
12
|
+
*/
|
|
13
|
+
type ChartLayoutCrosshairMode = "off" | "time" | "full";
|
|
14
|
+
/**
|
|
15
|
+
* Resolved synchronization behavior for a layout.
|
|
16
|
+
*/
|
|
17
|
+
interface ChartLayoutSyncOptions {
|
|
18
|
+
/** Synchronize visible time ranges. */
|
|
19
|
+
timeRange: boolean;
|
|
20
|
+
/** Crosshair synchronization mode. */
|
|
21
|
+
crosshair: ChartLayoutCrosshairMode;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Partial synchronization update. Boolean crosshair values map to `time` and `off`.
|
|
25
|
+
*/
|
|
26
|
+
interface ChartLayoutSyncUpdate {
|
|
27
|
+
/** Synchronize visible time ranges. */
|
|
28
|
+
timeRange?: boolean;
|
|
29
|
+
/** Crosshair synchronization mode. */
|
|
30
|
+
crosshair?: boolean | ChartLayoutCrosshairMode;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Options for a layout chart cell.
|
|
34
|
+
*/
|
|
35
|
+
interface ChartLayoutSlotOptions {
|
|
36
|
+
/** Stable slot id. Generated when omitted. */
|
|
37
|
+
id?: string;
|
|
38
|
+
/** Chart options for this slot. Merged over the layout defaults. */
|
|
39
|
+
chartOptions?: ChartOptions;
|
|
40
|
+
/** Number of grid rows covered by the cell. Defaults to 1. */
|
|
41
|
+
rowSpan?: number;
|
|
42
|
+
/** Number of grid columns covered by the cell. Defaults to 1. */
|
|
43
|
+
columnSpan?: number;
|
|
44
|
+
/** Synchronization group. Slots only sync with slots in the same group. */
|
|
45
|
+
syncGroup?: string;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* One chart cell in a layout.
|
|
49
|
+
*/
|
|
50
|
+
interface ChartLayoutSlot {
|
|
51
|
+
readonly id: string;
|
|
52
|
+
readonly chart: ChartApi;
|
|
53
|
+
readonly element: HTMLElement;
|
|
54
|
+
readonly rowSpan: number;
|
|
55
|
+
readonly columnSpan: number;
|
|
56
|
+
readonly syncGroup: string;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Serializable structural layout state. Chart state is owned by the host.
|
|
60
|
+
*/
|
|
61
|
+
interface ChartLayoutSnapshot {
|
|
62
|
+
version: 1;
|
|
63
|
+
columns: number;
|
|
64
|
+
gap: number;
|
|
65
|
+
sync: ChartLayoutSyncOptions;
|
|
66
|
+
slots: ReadonlyArray<{
|
|
67
|
+
id: string;
|
|
68
|
+
rowSpan: number;
|
|
69
|
+
columnSpan: number;
|
|
70
|
+
syncGroup: string;
|
|
71
|
+
}>;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Options for a multi-chart layout.
|
|
75
|
+
*/
|
|
76
|
+
interface ChartLayoutOptions {
|
|
77
|
+
/** Number of grid columns. Defaults to 2. */
|
|
78
|
+
columns?: number;
|
|
79
|
+
/** Gap between cells in CSS pixels. Defaults to 1. */
|
|
80
|
+
gap?: number;
|
|
81
|
+
/** Default chart options applied to every slot. */
|
|
82
|
+
chartOptions?: ChartOptions;
|
|
83
|
+
/** Synchronization options. */
|
|
84
|
+
sync?: ChartLayoutSyncUpdate;
|
|
85
|
+
/** Synchronize visible time ranges. Defaults to true. */
|
|
86
|
+
syncTimeRange?: boolean;
|
|
87
|
+
/** Crosshair synchronization. Defaults to `time`. */
|
|
88
|
+
syncCrosshair?: boolean | ChartLayoutCrosshairMode;
|
|
89
|
+
/** Crosshair line colour. */
|
|
90
|
+
crosshairColor?: string;
|
|
91
|
+
/** Extra class name on the layout root. */
|
|
92
|
+
className?: string;
|
|
93
|
+
/** Called when the active chart changes. */
|
|
94
|
+
onActiveChange?: (chartId: string | null) => void;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* A live multi-chart layout.
|
|
98
|
+
*/
|
|
99
|
+
interface ChartLayoutApi {
|
|
100
|
+
/** Layout root inserted into the host container. */
|
|
101
|
+
readonly element: HTMLElement;
|
|
102
|
+
/** Adds a chart cell and returns its live slot. */
|
|
103
|
+
addChart(options?: ChartLayoutSlotOptions): ChartLayoutSlot;
|
|
104
|
+
/** Destroys and removes one chart cell. */
|
|
105
|
+
removeChart(id: string): void;
|
|
106
|
+
/** Returns a live chart, or null when the id is unknown. */
|
|
107
|
+
getChart(id: string): ChartApi | null;
|
|
108
|
+
/** Returns the current slots in DOM order. */
|
|
109
|
+
getSlots(): readonly ChartLayoutSlot[];
|
|
110
|
+
/** Returns slot ids in DOM order. */
|
|
111
|
+
chartIds(): readonly string[];
|
|
112
|
+
/** Changes the grid column count. */
|
|
113
|
+
setColumns(columns: number): void;
|
|
114
|
+
/** Changes the cell gap in CSS pixels. */
|
|
115
|
+
setGap(gap: number): void;
|
|
116
|
+
/** Changes synchronization behavior without recreating charts. */
|
|
117
|
+
setSyncOptions(options: ChartLayoutSyncUpdate): void;
|
|
118
|
+
/** Returns the resolved synchronization behavior. */
|
|
119
|
+
getSyncOptions(): Readonly<ChartLayoutSyncOptions>;
|
|
120
|
+
/** Marks one chart as active. Pass null to clear. */
|
|
121
|
+
setActiveChart(id: string | null): void;
|
|
122
|
+
/** Returns the active chart id, or null. */
|
|
123
|
+
activeChartId(): string | null;
|
|
124
|
+
/** Subscribes to active chart changes. */
|
|
125
|
+
onActiveChange(handler: (chartId: string | null) => void): Unsubscribe;
|
|
126
|
+
/** Applies the active chart's visible range to every chart in its sync group. */
|
|
127
|
+
syncNow(): void;
|
|
128
|
+
/** Returns structural layout state for host persistence. */
|
|
129
|
+
serialize(): ChartLayoutSnapshot;
|
|
130
|
+
/** Destroys every chart and removes the layout root. */
|
|
131
|
+
destroy(): void;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Creates a framework-agnostic grid of Wicklib charts.
|
|
135
|
+
*
|
|
136
|
+
* The layout owns chart creation, grid placement, time-range synchronization, and a
|
|
137
|
+
* mirrored crosshair. It does not own data, symbol state, accounts, or persistence.
|
|
138
|
+
*/
|
|
139
|
+
declare function createChartLayout(container: HTMLElement, options?: ChartLayoutOptions): ChartLayoutApi;
|
|
140
|
+
|
|
141
|
+
export { createChartLayout };
|
|
142
|
+
export type { ChartLayoutApi, ChartLayoutCrosshairMode, ChartLayoutOptions, ChartLayoutSlot, ChartLayoutSlotOptions, ChartLayoutSnapshot, ChartLayoutSyncOptions, ChartLayoutSyncUpdate };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
import { WickError, createChart } from '@wicklib/core';
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
var DEFAULT_COLUMNS = 2;
|
|
5
|
+
var DEFAULT_GAP = 1;
|
|
6
|
+
var DEFAULT_SYNC_GROUP = "default";
|
|
7
|
+
var MAX_COLUMNS = 12;
|
|
8
|
+
var DEFAULT_CROSSHAIR_COLOR = "rgba(139, 149, 167, 0.6)";
|
|
9
|
+
function createChartLayout(container, options = {}) {
|
|
10
|
+
return new ChartLayout(container, options);
|
|
11
|
+
}
|
|
12
|
+
var ChartLayout = class {
|
|
13
|
+
element;
|
|
14
|
+
slots = [];
|
|
15
|
+
activeHandlers = /* @__PURE__ */ new Set();
|
|
16
|
+
crosshairOverlays = /* @__PURE__ */ new Map();
|
|
17
|
+
chartOptions;
|
|
18
|
+
crosshairColor;
|
|
19
|
+
columns;
|
|
20
|
+
gap;
|
|
21
|
+
sync;
|
|
22
|
+
activeId = null;
|
|
23
|
+
counter = 0;
|
|
24
|
+
applyingRange = false;
|
|
25
|
+
crosshair = null;
|
|
26
|
+
destroyed = false;
|
|
27
|
+
constructor(container, options) {
|
|
28
|
+
if (!container || typeof container.appendChild !== "function") {
|
|
29
|
+
throw new WickError("INVALID_ARGUMENT", "createChartLayout requires a DOM container");
|
|
30
|
+
}
|
|
31
|
+
this.columns = validateColumns(options.columns ?? DEFAULT_COLUMNS);
|
|
32
|
+
this.gap = validateGap(options.gap ?? DEFAULT_GAP);
|
|
33
|
+
this.chartOptions = options.chartOptions;
|
|
34
|
+
this.sync = resolveSyncOptions(options);
|
|
35
|
+
this.crosshairColor = options.crosshairColor ?? DEFAULT_CROSSHAIR_COLOR;
|
|
36
|
+
if (options.onActiveChange) {
|
|
37
|
+
this.activeHandlers.add(options.onActiveChange);
|
|
38
|
+
}
|
|
39
|
+
this.element = document.createElement("div");
|
|
40
|
+
this.element.className = ["wicklib-chart-layout", options.className].filter(Boolean).join(" ");
|
|
41
|
+
Object.assign(this.element.style, {
|
|
42
|
+
display: "grid",
|
|
43
|
+
width: "100%",
|
|
44
|
+
height: "100%",
|
|
45
|
+
minWidth: "0",
|
|
46
|
+
minHeight: "0",
|
|
47
|
+
gridAutoRows: "minmax(0, 1fr)",
|
|
48
|
+
gap: `${this.gap}px`
|
|
49
|
+
});
|
|
50
|
+
this.applyGrid();
|
|
51
|
+
container.appendChild(this.element);
|
|
52
|
+
}
|
|
53
|
+
addChart(options = {}) {
|
|
54
|
+
this.assertAlive();
|
|
55
|
+
const id = options.id ?? `chart-${++this.counter}`;
|
|
56
|
+
if (this.findSlot(id)) {
|
|
57
|
+
throw new WickError("INVALID_ARGUMENT", `Duplicate chart layout id: ${id}`);
|
|
58
|
+
}
|
|
59
|
+
const rowSpan = validateSpan(options.rowSpan ?? 1, "rowSpan");
|
|
60
|
+
const columnSpan = validateSpan(options.columnSpan ?? 1, "columnSpan");
|
|
61
|
+
const syncGroup = validateSyncGroup(options.syncGroup ?? DEFAULT_SYNC_GROUP);
|
|
62
|
+
const element = document.createElement("div");
|
|
63
|
+
element.className = "wicklib-chart-layout__cell";
|
|
64
|
+
element.dataset.chartId = id;
|
|
65
|
+
Object.assign(element.style, {
|
|
66
|
+
position: "relative",
|
|
67
|
+
minWidth: "0",
|
|
68
|
+
minHeight: "0",
|
|
69
|
+
overflow: "hidden",
|
|
70
|
+
gridRow: `span ${rowSpan}`,
|
|
71
|
+
gridColumn: `span ${columnSpan}`
|
|
72
|
+
});
|
|
73
|
+
this.element.appendChild(element);
|
|
74
|
+
let chart;
|
|
75
|
+
try {
|
|
76
|
+
chart = createChart(element, mergeChartOptions(this.chartOptions, options.chartOptions));
|
|
77
|
+
} catch (error) {
|
|
78
|
+
element.remove();
|
|
79
|
+
throw error;
|
|
80
|
+
}
|
|
81
|
+
const onPointerDown = () => this.setActiveChart(id);
|
|
82
|
+
const onFocusIn = () => this.setActiveChart(id);
|
|
83
|
+
element.addEventListener("pointerdown", onPointerDown, true);
|
|
84
|
+
element.addEventListener("focusin", onFocusIn);
|
|
85
|
+
const unsubscribes = [
|
|
86
|
+
chart.on("visible-range", (event) => this.handleVisibleRange(id, event)),
|
|
87
|
+
chart.on("crosshair", (event) => this.handleCrosshair(id, event))
|
|
88
|
+
];
|
|
89
|
+
const slot = {
|
|
90
|
+
id,
|
|
91
|
+
chart,
|
|
92
|
+
element,
|
|
93
|
+
rowSpan,
|
|
94
|
+
columnSpan,
|
|
95
|
+
syncGroup
|
|
96
|
+
};
|
|
97
|
+
const entry = {
|
|
98
|
+
slot,
|
|
99
|
+
unsubscribe: unsubscribes,
|
|
100
|
+
overlay: null,
|
|
101
|
+
onPointerDown,
|
|
102
|
+
onFocusIn
|
|
103
|
+
};
|
|
104
|
+
this.slots.push(entry);
|
|
105
|
+
this.ensureCrosshairOverlay(entry);
|
|
106
|
+
if (this.activeId === null) {
|
|
107
|
+
this.setActiveChart(id);
|
|
108
|
+
}
|
|
109
|
+
return slot;
|
|
110
|
+
}
|
|
111
|
+
removeChart(id) {
|
|
112
|
+
this.assertAlive();
|
|
113
|
+
const index = this.slots.findIndex((entry2) => entry2.slot.id === id);
|
|
114
|
+
if (index < 0) {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
const entry = this.slots[index];
|
|
118
|
+
this.disposeSlot(entry);
|
|
119
|
+
this.slots.splice(index, 1);
|
|
120
|
+
if (this.activeId === id) {
|
|
121
|
+
this.setActiveChart(null);
|
|
122
|
+
}
|
|
123
|
+
if (this.crosshair?.sourceId === id) {
|
|
124
|
+
this.crosshair = null;
|
|
125
|
+
this.requestCrosshairRender();
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
getChart(id) {
|
|
129
|
+
this.assertAlive();
|
|
130
|
+
return this.findSlot(id)?.slot.chart ?? null;
|
|
131
|
+
}
|
|
132
|
+
getSlots() {
|
|
133
|
+
this.assertAlive();
|
|
134
|
+
return this.slots.map((entry) => entry.slot);
|
|
135
|
+
}
|
|
136
|
+
chartIds() {
|
|
137
|
+
this.assertAlive();
|
|
138
|
+
return this.slots.map((entry) => entry.slot.id);
|
|
139
|
+
}
|
|
140
|
+
setColumns(columns) {
|
|
141
|
+
this.assertAlive();
|
|
142
|
+
this.columns = validateColumns(columns);
|
|
143
|
+
this.applyGrid();
|
|
144
|
+
}
|
|
145
|
+
setGap(gap) {
|
|
146
|
+
this.assertAlive();
|
|
147
|
+
this.gap = validateGap(gap);
|
|
148
|
+
this.element.style.gap = `${this.gap}px`;
|
|
149
|
+
}
|
|
150
|
+
setSyncOptions(options) {
|
|
151
|
+
this.assertAlive();
|
|
152
|
+
const next = {
|
|
153
|
+
timeRange: options.timeRange ?? this.sync.timeRange,
|
|
154
|
+
crosshair: options.crosshair === void 0 ? this.sync.crosshair : normalizeCrosshair(options.crosshair)
|
|
155
|
+
};
|
|
156
|
+
if (next.timeRange === this.sync.timeRange && next.crosshair === this.sync.crosshair) {
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
this.sync = next;
|
|
160
|
+
for (const entry of this.slots) {
|
|
161
|
+
this.ensureCrosshairOverlay(entry);
|
|
162
|
+
}
|
|
163
|
+
if (next.crosshair === "off" && this.crosshair !== null) {
|
|
164
|
+
this.crosshair = null;
|
|
165
|
+
}
|
|
166
|
+
this.requestCrosshairRender();
|
|
167
|
+
}
|
|
168
|
+
getSyncOptions() {
|
|
169
|
+
this.assertAlive();
|
|
170
|
+
return { ...this.sync };
|
|
171
|
+
}
|
|
172
|
+
setActiveChart(id) {
|
|
173
|
+
this.assertAlive();
|
|
174
|
+
if (id !== null && !this.findSlot(id)) {
|
|
175
|
+
throw new WickError("INVALID_ARGUMENT", `Unknown chart layout id: ${id}`);
|
|
176
|
+
}
|
|
177
|
+
if (this.activeId === id) {
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
const previous = this.activeId;
|
|
181
|
+
this.activeId = id;
|
|
182
|
+
for (const entry of this.slots) {
|
|
183
|
+
if (entry.slot.id === previous) {
|
|
184
|
+
delete entry.slot.element.dataset.active;
|
|
185
|
+
}
|
|
186
|
+
if (entry.slot.id === id) {
|
|
187
|
+
entry.slot.element.dataset.active = "true";
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
for (const handler of this.activeHandlers) {
|
|
191
|
+
handler(id);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
activeChartId() {
|
|
195
|
+
this.assertAlive();
|
|
196
|
+
return this.activeId;
|
|
197
|
+
}
|
|
198
|
+
onActiveChange(handler) {
|
|
199
|
+
this.assertAlive();
|
|
200
|
+
this.activeHandlers.add(handler);
|
|
201
|
+
return () => {
|
|
202
|
+
this.activeHandlers.delete(handler);
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
syncNow() {
|
|
206
|
+
this.assertAlive();
|
|
207
|
+
const source = this.findSlot(this.activeId ?? this.slots[0]?.slot.id ?? "");
|
|
208
|
+
if (!source) {
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
this.applyRange(
|
|
212
|
+
source.slot.id,
|
|
213
|
+
source.slot.syncGroup,
|
|
214
|
+
source.slot.chart.timeScale().getVisibleRange()
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
serialize() {
|
|
218
|
+
this.assertAlive();
|
|
219
|
+
return {
|
|
220
|
+
version: 1,
|
|
221
|
+
columns: this.columns,
|
|
222
|
+
gap: this.gap,
|
|
223
|
+
sync: { ...this.sync },
|
|
224
|
+
slots: this.slots.map((entry) => ({
|
|
225
|
+
id: entry.slot.id,
|
|
226
|
+
rowSpan: entry.slot.rowSpan,
|
|
227
|
+
columnSpan: entry.slot.columnSpan,
|
|
228
|
+
syncGroup: entry.slot.syncGroup
|
|
229
|
+
}))
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
destroy() {
|
|
233
|
+
if (this.destroyed) {
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
this.destroyed = true;
|
|
237
|
+
for (const entry of this.slots) {
|
|
238
|
+
this.disposeSlot(entry);
|
|
239
|
+
}
|
|
240
|
+
this.slots.length = 0;
|
|
241
|
+
this.activeHandlers.clear();
|
|
242
|
+
this.crosshairOverlays.clear();
|
|
243
|
+
this.element.remove();
|
|
244
|
+
}
|
|
245
|
+
applyGrid() {
|
|
246
|
+
this.element.style.gridTemplateColumns = `repeat(${this.columns}, minmax(0, 1fr))`;
|
|
247
|
+
}
|
|
248
|
+
findSlot(id) {
|
|
249
|
+
if (id === null) {
|
|
250
|
+
return void 0;
|
|
251
|
+
}
|
|
252
|
+
return this.slots.find((entry) => entry.slot.id === id);
|
|
253
|
+
}
|
|
254
|
+
handleVisibleRange(sourceId, event) {
|
|
255
|
+
if (!this.sync.timeRange || this.applyingRange) {
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
const source = this.findSlot(sourceId);
|
|
259
|
+
if (!source) {
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
this.applyRange(sourceId, source.slot.syncGroup, { from: event.from, to: event.to });
|
|
263
|
+
}
|
|
264
|
+
applyRange(sourceId, syncGroup, range) {
|
|
265
|
+
if (!Number.isFinite(range.from) || !Number.isFinite(range.to) || range.to <= range.from) {
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
this.applyingRange = true;
|
|
269
|
+
try {
|
|
270
|
+
for (const entry of this.slots) {
|
|
271
|
+
if (entry.slot.id === sourceId || entry.slot.syncGroup !== syncGroup) {
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
try {
|
|
275
|
+
entry.slot.chart.timeScale().setVisibleRange(range);
|
|
276
|
+
} catch {
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
} finally {
|
|
280
|
+
this.applyingRange = false;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
handleCrosshair(sourceId, event) {
|
|
284
|
+
if (this.sync.crosshair === "off") {
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
if (event.time === null || event.point === null) {
|
|
288
|
+
if (this.crosshair !== null) {
|
|
289
|
+
this.crosshair = null;
|
|
290
|
+
this.requestCrosshairRender();
|
|
291
|
+
}
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
const source = this.findSlot(sourceId);
|
|
295
|
+
if (!source) {
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
const height = source.slot.element.clientHeight;
|
|
299
|
+
const yRatio = height > 0 ? Math.min(1, Math.max(0, event.point.y / height)) : null;
|
|
300
|
+
this.crosshair = {
|
|
301
|
+
sourceId,
|
|
302
|
+
sourceGroup: source.slot.syncGroup,
|
|
303
|
+
time: event.time,
|
|
304
|
+
yRatio
|
|
305
|
+
};
|
|
306
|
+
this.requestCrosshairRender();
|
|
307
|
+
}
|
|
308
|
+
requestCrosshairRender() {
|
|
309
|
+
for (const overlay of this.crosshairOverlays.values()) {
|
|
310
|
+
overlay.requestRender();
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
ensureCrosshairOverlay(entry) {
|
|
314
|
+
if (this.sync.crosshair === "off") {
|
|
315
|
+
if (entry.overlay) {
|
|
316
|
+
entry.overlay.remove();
|
|
317
|
+
entry.overlay = null;
|
|
318
|
+
this.crosshairOverlays.delete(entry.slot.id);
|
|
319
|
+
}
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
if (entry.overlay) {
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
const id = entry.slot.id;
|
|
326
|
+
const group = entry.slot.syncGroup;
|
|
327
|
+
entry.overlay = entry.slot.chart.addOverlay({
|
|
328
|
+
id: "wicklib-layout-crosshair",
|
|
329
|
+
draw: (context, frame) => this.drawCrosshair(context, frame, id, group)
|
|
330
|
+
});
|
|
331
|
+
this.crosshairOverlays.set(id, entry.overlay);
|
|
332
|
+
}
|
|
333
|
+
drawCrosshair(context, frame, id, group) {
|
|
334
|
+
const current = this.crosshair;
|
|
335
|
+
if (!current || current.sourceId === id || current.sourceGroup !== group || this.sync.crosshair === "off") {
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
const x = frame.timeToX(current.time);
|
|
339
|
+
if (x === null || x < frame.plot.left || x > frame.plot.right) {
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
context.save();
|
|
343
|
+
context.beginPath();
|
|
344
|
+
context.rect(frame.plot.left, frame.plot.top, frame.plot.width, frame.plot.height);
|
|
345
|
+
context.clip();
|
|
346
|
+
context.strokeStyle = this.crosshairColor;
|
|
347
|
+
context.lineWidth = 1;
|
|
348
|
+
context.setLineDash([3, 4]);
|
|
349
|
+
const crispX = Math.round(x) + 0.5;
|
|
350
|
+
context.beginPath();
|
|
351
|
+
context.moveTo(crispX, frame.plot.top);
|
|
352
|
+
context.lineTo(crispX, frame.plot.bottom);
|
|
353
|
+
context.stroke();
|
|
354
|
+
if (this.sync.crosshair === "full" && current.yRatio !== null) {
|
|
355
|
+
const y = frame.plot.top + current.yRatio * frame.plot.height;
|
|
356
|
+
const crispY = Math.round(y) + 0.5;
|
|
357
|
+
context.beginPath();
|
|
358
|
+
context.moveTo(frame.plot.left, crispY);
|
|
359
|
+
context.lineTo(frame.plot.right, crispY);
|
|
360
|
+
context.stroke();
|
|
361
|
+
}
|
|
362
|
+
context.restore();
|
|
363
|
+
}
|
|
364
|
+
disposeSlot(entry) {
|
|
365
|
+
entry.slot.element.removeEventListener("pointerdown", entry.onPointerDown, true);
|
|
366
|
+
entry.slot.element.removeEventListener("focusin", entry.onFocusIn);
|
|
367
|
+
for (const unsubscribe of entry.unsubscribe) {
|
|
368
|
+
unsubscribe();
|
|
369
|
+
}
|
|
370
|
+
entry.overlay?.remove();
|
|
371
|
+
entry.overlay = null;
|
|
372
|
+
this.crosshairOverlays.delete(entry.slot.id);
|
|
373
|
+
entry.slot.chart.destroy();
|
|
374
|
+
entry.slot.element.remove();
|
|
375
|
+
}
|
|
376
|
+
assertAlive() {
|
|
377
|
+
if (this.destroyed) {
|
|
378
|
+
throw new WickError("DISPOSED", "Chart layout is disposed");
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
};
|
|
382
|
+
function resolveSyncOptions(options) {
|
|
383
|
+
return {
|
|
384
|
+
timeRange: options.sync?.timeRange ?? options.syncTimeRange ?? true,
|
|
385
|
+
crosshair: normalizeCrosshair(options.sync?.crosshair ?? options.syncCrosshair ?? "time")
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
function normalizeCrosshair(value) {
|
|
389
|
+
if (value === void 0 || value === true || value === "time") {
|
|
390
|
+
return "time";
|
|
391
|
+
}
|
|
392
|
+
if (value === false || value === "off") {
|
|
393
|
+
return "off";
|
|
394
|
+
}
|
|
395
|
+
if (value === "full") {
|
|
396
|
+
return "full";
|
|
397
|
+
}
|
|
398
|
+
throw new WickError("INVALID_ARGUMENT", "crosshair must be off, time, or full");
|
|
399
|
+
}
|
|
400
|
+
function mergeChartOptions(base, override) {
|
|
401
|
+
if (!base) {
|
|
402
|
+
return override;
|
|
403
|
+
}
|
|
404
|
+
if (!override) {
|
|
405
|
+
return base;
|
|
406
|
+
}
|
|
407
|
+
return { ...base, ...override };
|
|
408
|
+
}
|
|
409
|
+
function validateColumns(columns) {
|
|
410
|
+
if (!Number.isInteger(columns) || columns < 1 || columns > MAX_COLUMNS) {
|
|
411
|
+
throw new WickError("INVALID_ARGUMENT", `columns must be an integer from 1 to ${MAX_COLUMNS}`);
|
|
412
|
+
}
|
|
413
|
+
return columns;
|
|
414
|
+
}
|
|
415
|
+
function validateGap(gap) {
|
|
416
|
+
if (!Number.isFinite(gap) || gap < 0) {
|
|
417
|
+
throw new WickError("INVALID_ARGUMENT", "gap must be a non-negative finite number");
|
|
418
|
+
}
|
|
419
|
+
return gap;
|
|
420
|
+
}
|
|
421
|
+
function validateSpan(value, name) {
|
|
422
|
+
if (!Number.isInteger(value) || value < 1) {
|
|
423
|
+
throw new WickError("INVALID_ARGUMENT", `${name} must be a positive integer`);
|
|
424
|
+
}
|
|
425
|
+
return value;
|
|
426
|
+
}
|
|
427
|
+
function validateSyncGroup(value) {
|
|
428
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
429
|
+
throw new WickError("INVALID_ARGUMENT", "syncGroup must be a non-empty string");
|
|
430
|
+
}
|
|
431
|
+
return value;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
export { createChartLayout };
|
|
435
|
+
//# sourceMappingURL=index.js.map
|
|
436
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"names":["entry"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Free multi-chart layout primitives for Wicklib.\n */\n\nimport {\n WickError,\n createChart,\n type ChartApi,\n type ChartOptions,\n type ChartOverlayApi,\n type CrosshairEvent,\n type OverlayRenderFrame,\n type Unsubscribe,\n type VisibleRangeEvent,\n} from \"@wicklib/core\";\n\n/**\n * Crosshair synchronization mode.\n *\n * `time` mirrors the vertical time line. `full` also mirrors a horizontal line at the same\n * relative pane height.\n */\nexport type ChartLayoutCrosshairMode = \"off\" | \"time\" | \"full\";\n\n/**\n * Resolved synchronization behavior for a layout.\n */\nexport interface ChartLayoutSyncOptions {\n /** Synchronize visible time ranges. */\n timeRange: boolean;\n /** Crosshair synchronization mode. */\n crosshair: ChartLayoutCrosshairMode;\n}\n\n/**\n * Partial synchronization update. Boolean crosshair values map to `time` and `off`.\n */\nexport interface ChartLayoutSyncUpdate {\n /** Synchronize visible time ranges. */\n timeRange?: boolean;\n /** Crosshair synchronization mode. */\n crosshair?: boolean | ChartLayoutCrosshairMode;\n}\n\n/**\n * Options for a layout chart cell.\n */\nexport interface ChartLayoutSlotOptions {\n /** Stable slot id. Generated when omitted. */\n id?: string;\n /** Chart options for this slot. Merged over the layout defaults. */\n chartOptions?: ChartOptions;\n /** Number of grid rows covered by the cell. Defaults to 1. */\n rowSpan?: number;\n /** Number of grid columns covered by the cell. Defaults to 1. */\n columnSpan?: number;\n /** Synchronization group. Slots only sync with slots in the same group. */\n syncGroup?: string;\n}\n\n/**\n * One chart cell in a layout.\n */\nexport interface ChartLayoutSlot {\n readonly id: string;\n readonly chart: ChartApi;\n readonly element: HTMLElement;\n readonly rowSpan: number;\n readonly columnSpan: number;\n readonly syncGroup: string;\n}\n\n/**\n * Serializable structural layout state. Chart state is owned by the host.\n */\nexport interface ChartLayoutSnapshot {\n version: 1;\n columns: number;\n gap: number;\n sync: ChartLayoutSyncOptions;\n slots: ReadonlyArray<{\n id: string;\n rowSpan: number;\n columnSpan: number;\n syncGroup: string;\n }>;\n}\n\n/**\n * Options for a multi-chart layout.\n */\nexport interface ChartLayoutOptions {\n /** Number of grid columns. Defaults to 2. */\n columns?: number;\n /** Gap between cells in CSS pixels. Defaults to 1. */\n gap?: number;\n /** Default chart options applied to every slot. */\n chartOptions?: ChartOptions;\n /** Synchronization options. */\n sync?: ChartLayoutSyncUpdate;\n /** Synchronize visible time ranges. Defaults to true. */\n syncTimeRange?: boolean;\n /** Crosshair synchronization. Defaults to `time`. */\n syncCrosshair?: boolean | ChartLayoutCrosshairMode;\n /** Crosshair line colour. */\n crosshairColor?: string;\n /** Extra class name on the layout root. */\n className?: string;\n /** Called when the active chart changes. */\n onActiveChange?: (chartId: string | null) => void;\n}\n\n/**\n * A live multi-chart layout.\n */\nexport interface ChartLayoutApi {\n /** Layout root inserted into the host container. */\n readonly element: HTMLElement;\n /** Adds a chart cell and returns its live slot. */\n addChart(options?: ChartLayoutSlotOptions): ChartLayoutSlot;\n /** Destroys and removes one chart cell. */\n removeChart(id: string): void;\n /** Returns a live chart, or null when the id is unknown. */\n getChart(id: string): ChartApi | null;\n /** Returns the current slots in DOM order. */\n getSlots(): readonly ChartLayoutSlot[];\n /** Returns slot ids in DOM order. */\n chartIds(): readonly string[];\n /** Changes the grid column count. */\n setColumns(columns: number): void;\n /** Changes the cell gap in CSS pixels. */\n setGap(gap: number): void;\n /** Changes synchronization behavior without recreating charts. */\n setSyncOptions(options: ChartLayoutSyncUpdate): void;\n /** Returns the resolved synchronization behavior. */\n getSyncOptions(): Readonly<ChartLayoutSyncOptions>;\n /** Marks one chart as active. Pass null to clear. */\n setActiveChart(id: string | null): void;\n /** Returns the active chart id, or null. */\n activeChartId(): string | null;\n /** Subscribes to active chart changes. */\n onActiveChange(handler: (chartId: string | null) => void): Unsubscribe;\n /** Applies the active chart's visible range to every chart in its sync group. */\n syncNow(): void;\n /** Returns structural layout state for host persistence. */\n serialize(): ChartLayoutSnapshot;\n /** Destroys every chart and removes the layout root. */\n destroy(): void;\n}\n\ninterface InternalSlot {\n readonly slot: ChartLayoutSlot;\n readonly unsubscribe: readonly Unsubscribe[];\n overlay: ChartOverlayApi | null;\n readonly onPointerDown: () => void;\n readonly onFocusIn: () => void;\n}\n\ninterface CrosshairState {\n sourceId: string;\n sourceGroup: string;\n time: number;\n yRatio: number | null;\n}\n\nconst DEFAULT_COLUMNS = 2;\nconst DEFAULT_GAP = 1;\nconst DEFAULT_SYNC_GROUP = \"default\";\nconst MAX_COLUMNS = 12;\nconst DEFAULT_CROSSHAIR_COLOR = \"rgba(139, 149, 167, 0.6)\";\n\n/**\n * Creates a framework-agnostic grid of Wicklib charts.\n *\n * The layout owns chart creation, grid placement, time-range synchronization, and a\n * mirrored crosshair. It does not own data, symbol state, accounts, or persistence.\n */\nexport function createChartLayout(\n container: HTMLElement,\n options: ChartLayoutOptions = {},\n): ChartLayoutApi {\n return new ChartLayout(container, options);\n}\n\nclass ChartLayout implements ChartLayoutApi {\n readonly element: HTMLElement;\n\n private readonly slots: InternalSlot[] = [];\n private readonly activeHandlers = new Set<(chartId: string | null) => void>();\n private readonly crosshairOverlays = new Map<string, ChartOverlayApi>();\n private readonly chartOptions: ChartOptions | undefined;\n private readonly crosshairColor: string;\n private columns: number;\n private gap: number;\n private sync: ChartLayoutSyncOptions;\n private activeId: string | null = null;\n private counter = 0;\n private applyingRange = false;\n private crosshair: CrosshairState | null = null;\n private destroyed = false;\n\n constructor(container: HTMLElement, options: ChartLayoutOptions) {\n if (!container || typeof container.appendChild !== \"function\") {\n throw new WickError(\"INVALID_ARGUMENT\", \"createChartLayout requires a DOM container\");\n }\n\n this.columns = validateColumns(options.columns ?? DEFAULT_COLUMNS);\n this.gap = validateGap(options.gap ?? DEFAULT_GAP);\n this.chartOptions = options.chartOptions;\n this.sync = resolveSyncOptions(options);\n this.crosshairColor = options.crosshairColor ?? DEFAULT_CROSSHAIR_COLOR;\n if (options.onActiveChange) {\n this.activeHandlers.add(options.onActiveChange);\n }\n\n this.element = document.createElement(\"div\");\n this.element.className = [\"wicklib-chart-layout\", options.className].filter(Boolean).join(\" \");\n Object.assign(this.element.style, {\n display: \"grid\",\n width: \"100%\",\n height: \"100%\",\n minWidth: \"0\",\n minHeight: \"0\",\n gridAutoRows: \"minmax(0, 1fr)\",\n gap: `${this.gap}px`,\n });\n this.applyGrid();\n container.appendChild(this.element);\n }\n\n addChart(options: ChartLayoutSlotOptions = {}): ChartLayoutSlot {\n this.assertAlive();\n const id = options.id ?? `chart-${++this.counter}`;\n if (this.findSlot(id)) {\n throw new WickError(\"INVALID_ARGUMENT\", `Duplicate chart layout id: ${id}`);\n }\n const rowSpan = validateSpan(options.rowSpan ?? 1, \"rowSpan\");\n const columnSpan = validateSpan(options.columnSpan ?? 1, \"columnSpan\");\n const syncGroup = validateSyncGroup(options.syncGroup ?? DEFAULT_SYNC_GROUP);\n\n const element = document.createElement(\"div\");\n element.className = \"wicklib-chart-layout__cell\";\n element.dataset.chartId = id;\n Object.assign(element.style, {\n position: \"relative\",\n minWidth: \"0\",\n minHeight: \"0\",\n overflow: \"hidden\",\n gridRow: `span ${rowSpan}`,\n gridColumn: `span ${columnSpan}`,\n });\n this.element.appendChild(element);\n\n let chart: ChartApi;\n try {\n chart = createChart(element, mergeChartOptions(this.chartOptions, options.chartOptions));\n } catch (error) {\n element.remove();\n throw error;\n }\n\n const onPointerDown = (): void => this.setActiveChart(id);\n const onFocusIn = (): void => this.setActiveChart(id);\n element.addEventListener(\"pointerdown\", onPointerDown, true);\n element.addEventListener(\"focusin\", onFocusIn);\n\n const unsubscribes: Unsubscribe[] = [\n chart.on(\"visible-range\", (event) => this.handleVisibleRange(id, event)),\n chart.on(\"crosshair\", (event) => this.handleCrosshair(id, event)),\n ];\n\n const slot: ChartLayoutSlot = {\n id,\n chart,\n element,\n rowSpan,\n columnSpan,\n syncGroup,\n };\n const entry: InternalSlot = {\n slot,\n unsubscribe: unsubscribes,\n overlay: null,\n onPointerDown,\n onFocusIn,\n };\n this.slots.push(entry);\n this.ensureCrosshairOverlay(entry);\n\n if (this.activeId === null) {\n this.setActiveChart(id);\n }\n\n return slot;\n }\n\n removeChart(id: string): void {\n this.assertAlive();\n const index = this.slots.findIndex((entry) => entry.slot.id === id);\n if (index < 0) {\n return;\n }\n const entry = this.slots[index]!;\n this.disposeSlot(entry);\n this.slots.splice(index, 1);\n if (this.activeId === id) {\n this.setActiveChart(null);\n }\n if (this.crosshair?.sourceId === id) {\n this.crosshair = null;\n this.requestCrosshairRender();\n }\n }\n\n getChart(id: string): ChartApi | null {\n this.assertAlive();\n return this.findSlot(id)?.slot.chart ?? null;\n }\n\n getSlots(): readonly ChartLayoutSlot[] {\n this.assertAlive();\n return this.slots.map((entry) => entry.slot);\n }\n\n chartIds(): readonly string[] {\n this.assertAlive();\n return this.slots.map((entry) => entry.slot.id);\n }\n\n setColumns(columns: number): void {\n this.assertAlive();\n this.columns = validateColumns(columns);\n this.applyGrid();\n }\n\n setGap(gap: number): void {\n this.assertAlive();\n this.gap = validateGap(gap);\n this.element.style.gap = `${this.gap}px`;\n }\n\n setSyncOptions(options: ChartLayoutSyncUpdate): void {\n this.assertAlive();\n const next: ChartLayoutSyncOptions = {\n timeRange: options.timeRange ?? this.sync.timeRange,\n crosshair:\n options.crosshair === undefined\n ? this.sync.crosshair\n : normalizeCrosshair(options.crosshair),\n };\n if (next.timeRange === this.sync.timeRange && next.crosshair === this.sync.crosshair) {\n return;\n }\n this.sync = next;\n for (const entry of this.slots) {\n this.ensureCrosshairOverlay(entry);\n }\n if (next.crosshair === \"off\" && this.crosshair !== null) {\n this.crosshair = null;\n }\n this.requestCrosshairRender();\n }\n\n getSyncOptions(): Readonly<ChartLayoutSyncOptions> {\n this.assertAlive();\n return { ...this.sync };\n }\n\n setActiveChart(id: string | null): void {\n this.assertAlive();\n if (id !== null && !this.findSlot(id)) {\n throw new WickError(\"INVALID_ARGUMENT\", `Unknown chart layout id: ${id}`);\n }\n if (this.activeId === id) {\n return;\n }\n const previous = this.activeId;\n this.activeId = id;\n for (const entry of this.slots) {\n if (entry.slot.id === previous) {\n delete entry.slot.element.dataset.active;\n }\n if (entry.slot.id === id) {\n entry.slot.element.dataset.active = \"true\";\n }\n }\n for (const handler of this.activeHandlers) {\n handler(id);\n }\n }\n\n activeChartId(): string | null {\n this.assertAlive();\n return this.activeId;\n }\n\n onActiveChange(handler: (chartId: string | null) => void): Unsubscribe {\n this.assertAlive();\n this.activeHandlers.add(handler);\n return () => {\n this.activeHandlers.delete(handler);\n };\n }\n\n syncNow(): void {\n this.assertAlive();\n const source = this.findSlot(this.activeId ?? this.slots[0]?.slot.id ?? \"\");\n if (!source) {\n return;\n }\n this.applyRange(\n source.slot.id,\n source.slot.syncGroup,\n source.slot.chart.timeScale().getVisibleRange(),\n );\n }\n\n serialize(): ChartLayoutSnapshot {\n this.assertAlive();\n return {\n version: 1,\n columns: this.columns,\n gap: this.gap,\n sync: { ...this.sync },\n slots: this.slots.map((entry) => ({\n id: entry.slot.id,\n rowSpan: entry.slot.rowSpan,\n columnSpan: entry.slot.columnSpan,\n syncGroup: entry.slot.syncGroup,\n })),\n };\n }\n\n destroy(): void {\n if (this.destroyed) {\n return;\n }\n this.destroyed = true;\n for (const entry of this.slots) {\n this.disposeSlot(entry);\n }\n this.slots.length = 0;\n this.activeHandlers.clear();\n this.crosshairOverlays.clear();\n this.element.remove();\n }\n\n private applyGrid(): void {\n this.element.style.gridTemplateColumns = `repeat(${this.columns}, minmax(0, 1fr))`;\n }\n\n private findSlot(id: string | null): InternalSlot | undefined {\n if (id === null) {\n return undefined;\n }\n return this.slots.find((entry) => entry.slot.id === id);\n }\n\n private handleVisibleRange(sourceId: string, event: VisibleRangeEvent): void {\n if (!this.sync.timeRange || this.applyingRange) {\n return;\n }\n const source = this.findSlot(sourceId);\n if (!source) {\n return;\n }\n this.applyRange(sourceId, source.slot.syncGroup, { from: event.from, to: event.to });\n }\n\n private applyRange(\n sourceId: string,\n syncGroup: string,\n range: { from: number; to: number },\n ): void {\n if (!Number.isFinite(range.from) || !Number.isFinite(range.to) || range.to <= range.from) {\n return;\n }\n this.applyingRange = true;\n try {\n for (const entry of this.slots) {\n if (entry.slot.id === sourceId || entry.slot.syncGroup !== syncGroup) {\n continue;\n }\n try {\n entry.slot.chart.timeScale().setVisibleRange(range);\n } catch {\n // A chart with incompatible data can reject the range; leave it unchanged.\n }\n }\n } finally {\n this.applyingRange = false;\n }\n }\n\n private handleCrosshair(sourceId: string, event: CrosshairEvent): void {\n if (this.sync.crosshair === \"off\") {\n return;\n }\n if (event.time === null || event.point === null) {\n if (this.crosshair !== null) {\n this.crosshair = null;\n this.requestCrosshairRender();\n }\n return;\n }\n const source = this.findSlot(sourceId);\n if (!source) {\n return;\n }\n const height = source.slot.element.clientHeight;\n const yRatio = height > 0 ? Math.min(1, Math.max(0, event.point.y / height)) : null;\n this.crosshair = {\n sourceId,\n sourceGroup: source.slot.syncGroup,\n time: event.time,\n yRatio,\n };\n this.requestCrosshairRender();\n }\n\n private requestCrosshairRender(): void {\n for (const overlay of this.crosshairOverlays.values()) {\n overlay.requestRender();\n }\n }\n\n private ensureCrosshairOverlay(entry: InternalSlot): void {\n if (this.sync.crosshair === \"off\") {\n if (entry.overlay) {\n entry.overlay.remove();\n entry.overlay = null;\n this.crosshairOverlays.delete(entry.slot.id);\n }\n return;\n }\n if (entry.overlay) {\n return;\n }\n const id = entry.slot.id;\n const group = entry.slot.syncGroup;\n entry.overlay = entry.slot.chart.addOverlay({\n id: \"wicklib-layout-crosshair\",\n draw: (context, frame) => this.drawCrosshair(context, frame, id, group),\n });\n this.crosshairOverlays.set(id, entry.overlay);\n }\n\n private drawCrosshair(\n context: CanvasRenderingContext2D,\n frame: OverlayRenderFrame,\n id: string,\n group: string,\n ): void {\n const current = this.crosshair;\n if (\n !current ||\n current.sourceId === id ||\n current.sourceGroup !== group ||\n this.sync.crosshair === \"off\"\n ) {\n return;\n }\n const x = frame.timeToX(current.time);\n if (x === null || x < frame.plot.left || x > frame.plot.right) {\n return;\n }\n context.save();\n context.beginPath();\n context.rect(frame.plot.left, frame.plot.top, frame.plot.width, frame.plot.height);\n context.clip();\n context.strokeStyle = this.crosshairColor;\n context.lineWidth = 1;\n context.setLineDash([3, 4]);\n\n const crispX = Math.round(x) + 0.5;\n context.beginPath();\n context.moveTo(crispX, frame.plot.top);\n context.lineTo(crispX, frame.plot.bottom);\n context.stroke();\n\n if (this.sync.crosshair === \"full\" && current.yRatio !== null) {\n const y = frame.plot.top + current.yRatio * frame.plot.height;\n const crispY = Math.round(y) + 0.5;\n context.beginPath();\n context.moveTo(frame.plot.left, crispY);\n context.lineTo(frame.plot.right, crispY);\n context.stroke();\n }\n\n context.restore();\n }\n\n private disposeSlot(entry: InternalSlot): void {\n entry.slot.element.removeEventListener(\"pointerdown\", entry.onPointerDown, true);\n entry.slot.element.removeEventListener(\"focusin\", entry.onFocusIn);\n for (const unsubscribe of entry.unsubscribe) {\n unsubscribe();\n }\n entry.overlay?.remove();\n entry.overlay = null;\n this.crosshairOverlays.delete(entry.slot.id);\n entry.slot.chart.destroy();\n entry.slot.element.remove();\n }\n\n private assertAlive(): void {\n if (this.destroyed) {\n throw new WickError(\"DISPOSED\", \"Chart layout is disposed\");\n }\n }\n}\n\nfunction resolveSyncOptions(options: ChartLayoutOptions): ChartLayoutSyncOptions {\n return {\n timeRange: options.sync?.timeRange ?? options.syncTimeRange ?? true,\n crosshair: normalizeCrosshair(options.sync?.crosshair ?? options.syncCrosshair ?? \"time\"),\n };\n}\n\nfunction normalizeCrosshair(\n value: boolean | ChartLayoutCrosshairMode | undefined,\n): ChartLayoutCrosshairMode {\n if (value === undefined || value === true || value === \"time\") {\n return \"time\";\n }\n if (value === false || value === \"off\") {\n return \"off\";\n }\n if (value === \"full\") {\n return \"full\";\n }\n throw new WickError(\"INVALID_ARGUMENT\", \"crosshair must be off, time, or full\");\n}\n\nfunction mergeChartOptions(\n base: ChartOptions | undefined,\n override: ChartOptions | undefined,\n): ChartOptions | undefined {\n if (!base) {\n return override;\n }\n if (!override) {\n return base;\n }\n return { ...base, ...override };\n}\n\nfunction validateColumns(columns: number): number {\n if (!Number.isInteger(columns) || columns < 1 || columns > MAX_COLUMNS) {\n throw new WickError(\"INVALID_ARGUMENT\", `columns must be an integer from 1 to ${MAX_COLUMNS}`);\n }\n return columns;\n}\n\nfunction validateGap(gap: number): number {\n if (!Number.isFinite(gap) || gap < 0) {\n throw new WickError(\"INVALID_ARGUMENT\", \"gap must be a non-negative finite number\");\n }\n return gap;\n}\n\nfunction validateSpan(value: number, name: string): number {\n if (!Number.isInteger(value) || value < 1) {\n throw new WickError(\"INVALID_ARGUMENT\", `${name} must be a positive integer`);\n }\n return value;\n}\n\nfunction validateSyncGroup(value: string): string {\n if (typeof value !== \"string\" || value.trim() === \"\") {\n throw new WickError(\"INVALID_ARGUMENT\", \"syncGroup must be a non-empty string\");\n }\n return value;\n}\n"]}
|