@dschz/solid-uplot 0.2.0 → 0.4.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.
@@ -30,11 +30,13 @@ type SolidUplotPlugin<T extends VoidStruct = VoidStruct> = uPlot$1.Plugin | Uplo
30
30
  *
31
31
  * @template T - The type of the plugin bus data structure
32
32
  */
33
- type SolidUplotOptions<T extends VoidStruct = VoidStruct> = Omit<uPlot$1.Options, "plugins" | "width" | "height"> & {
33
+ type SolidUplotOptions<T extends VoidStruct = VoidStruct> = Omit<uPlot$1.Options, "plugins" | "width" | "height" | "data"> & {
34
34
  /** Chart width in pixels */
35
35
  readonly width?: number;
36
36
  /** Chart height in pixels */
37
37
  readonly height?: number;
38
+ /** Chart data - accepts AlignedData or number[][] */
39
+ readonly data?: uPlot$1.AlignedData | number[][];
38
40
  /** Plugin communication bus for coordinating between plugins */
39
41
  readonly pluginBus?: SolidUplotPluginBus<T>;
40
42
  /** Array of plugins to apply to the chart */
@@ -9,10 +9,7 @@ import uPlot from 'uplot';
9
9
 
10
10
  var createPluginBus = (initialData = {}) => {
11
11
  const [data, setData] = createStore(initialData);
12
- return {
13
- data,
14
- setData
15
- };
12
+ return { data, setData };
16
13
  };
17
14
 
18
15
  // src/eventPlugins.ts
@@ -1,7 +1,7 @@
1
1
  import { U as UplotPluginFactory, S as SolidUplotPluginBus } from '../createPluginBus-DdrjQANs.js';
2
2
  import { C as CursorData, S as SeriesDatum } from '../getSeriesData-04wGQord.js';
3
3
  import { Component, JSX } from 'solid-js';
4
- import uPlot from 'uplot';
4
+ import uPlot$1 from 'uplot';
5
5
  import 'solid-js/store';
6
6
 
7
7
  /**
@@ -156,7 +156,7 @@ type FocusSeriesPluginMessageBus = {
156
156
  /**
157
157
  * Configuration options for the focus series plugin.
158
158
  */
159
- type FocusSeriesPluginOptions = {
159
+ type FocusSeriesPluginOptions<T extends CursorPluginMessageBus & FocusSeriesPluginMessageBus> = {
160
160
  /**
161
161
  * The vertical distance in pixels required to focus a series.
162
162
  * If the cursor's Y position is within this threshold of a Y value, that series is considered focused.
@@ -183,6 +183,24 @@ type FocusSeriesPluginOptions = {
183
183
  * @default false
184
184
  */
185
185
  readonly rebuildPaths?: boolean;
186
+ /**
187
+ *
188
+ * For charts that are configured with this plugin, this callback is used to determine if the chart
189
+ * should redraw when the plugin bus updates. It is an additional condition to the base condition `cursor?.sourceId !== u.root.id`
190
+ * which is always applied first to prevent the source chart from redrawing twice.
191
+ *
192
+ * This can be used to add filtering logic, such as only reacting to specific source charts.
193
+ *
194
+ * @default undefined (always redraw for non-source charts)
195
+ */
196
+ readonly shouldRedrawOnBusUpdate?: (params: {
197
+ /** The current chart instance (the one evaluating whether to redraw) */
198
+ readonly u: uPlot;
199
+ /** Current cursor state from the bus (may be from another chart) */
200
+ readonly cursor: T["cursor"];
201
+ /** Current focus series state from the bus */
202
+ readonly focusSeries: T["focusSeries"];
203
+ }) => boolean;
186
204
  };
187
205
  /**
188
206
  * Creates a focus series plugin that visually highlights series based on cursor proximity.
@@ -219,7 +237,7 @@ type FocusSeriesPluginOptions = {
219
237
  * @param options - Configuration options for focus behavior
220
238
  * @returns A plugin factory function that creates the focus series plugin instance
221
239
  */
222
- declare const focusSeries: (options?: FocusSeriesPluginOptions) => UplotPluginFactory<CursorPluginMessageBus & FocusSeriesPluginMessageBus>;
240
+ declare const focusSeries: (options?: FocusSeriesPluginOptions<CursorPluginMessageBus & FocusSeriesPluginMessageBus>) => UplotPluginFactory<CursorPluginMessageBus & FocusSeriesPluginMessageBus>;
223
241
 
224
242
  /**
225
243
  * Simple legend placement options - only top corners to avoid axis conflicts
@@ -233,7 +251,7 @@ type LegendPlacement = "top-left" | "top-right";
233
251
  * data flow where series data changes immediately trigger legend updates with fresh data information.
234
252
  */
235
253
  type LegendProps = {
236
- readonly u: uPlot;
254
+ readonly u: uPlot$1;
237
255
  readonly seriesData: SeriesDatum[];
238
256
  readonly bus: SolidUplotPluginBus<CursorPluginMessageBus & FocusSeriesPluginMessageBus>;
239
257
  };
@@ -306,7 +324,7 @@ type TooltipCursorPlacement = "top-left" | "top-right" | "bottom-left" | "bottom
306
324
  */
307
325
  type TooltipProps = {
308
326
  /** The uPlot instance for accessing chart configuration and data */
309
- readonly u: uPlot;
327
+ readonly u: uPlot$1;
310
328
  /**
311
329
  * Current cursor data including position and data index, automatically updated
312
330
  * by the cursor plugin and passed via the plugin bus system
@@ -360,6 +378,36 @@ type TooltipConfigOptions = {
360
378
  * @default "top-left"
361
379
  */
362
380
  readonly placement?: TooltipCursorPlacement;
381
+ /**
382
+ * Use fixed positioning for the tooltip. Set to true when the chart is in a
383
+ * fixed positioning context (like a dialog or modal) to prevent tooltip clipping.
384
+ * @default false
385
+ */
386
+ readonly fixed?: boolean;
387
+ /**
388
+ * Optional callback to process or modify the calculated tooltip position.
389
+ * Receives the calculated position and placement preference, and should return a position object with the same structure.
390
+ * Use this to implement custom positioning logic or constraints.
391
+ *
392
+ * @param position - The calculated position with left and top coordinates
393
+ * @param placement - The placement preference that was used for calculation
394
+ * @returns Modified position object with left and top coordinates
395
+ *
396
+ * @example
397
+ * ```ts
398
+ * onPositionCalculated: (position, placement) => ({
399
+ * left: Math.max(0, position.left), // Prevent negative positioning
400
+ * top: placement.includes('top') ? position.top - 5 : position.top + 10
401
+ * })
402
+ * ```
403
+ */
404
+ readonly onPositionCalculated?: (position: {
405
+ left: number;
406
+ top: number;
407
+ }, placement: TooltipCursorPlacement) => {
408
+ left: number;
409
+ top: number;
410
+ };
363
411
  };
364
412
  /**
365
413
  * Combined options for the tooltip plugin including container props and behavior config.
@@ -376,6 +424,7 @@ type TooltipPluginOptions = TooltipRootProps & TooltipConfigOptions;
376
424
  * - Automatic positioning with edge detection and flipping
377
425
  * - Scroll-aware positioning that works with page scrolling
378
426
  * - Configurable placement preferences
427
+ * - Position callback for custom positioning logic or overrides
379
428
  * - Accessible tooltip with proper ARIA attributes
380
429
  * - Automatic cleanup and memory management
381
430
  *
@@ -58,7 +58,7 @@ var seriesFocusRedraw = (u, options = {}) => {
58
58
  } = options;
59
59
  for (let i = 1; i < u.series.length; i++) {
60
60
  const s = u.series[i];
61
- if (!focusTargets || !focusTargets.length) {
61
+ if (!focusTargets?.length) {
62
62
  s.alpha = 1;
63
63
  continue;
64
64
  }
@@ -104,13 +104,15 @@ var focusSeries = (options = {}) => {
104
104
  dispose = createRoot((dispose2) => {
105
105
  createEffect(() => {
106
106
  const cursor2 = bus.data.cursor;
107
- const focus = bus.data.focusSeries;
108
- if (cursor2?.sourceId !== u.root.id) {
107
+ const focusSeries2 = bus.data.focusSeries;
108
+ const isNotSourceChart = cursor2?.sourceId !== u.root.id;
109
+ const userRedrawCondition = options.shouldRedrawOnBusUpdate?.({ u, cursor: cursor2, focusSeries: focusSeries2 }) ?? true;
110
+ if (isNotSourceChart && userRedrawCondition) {
109
111
  seriesFocusRedraw(u, {
110
112
  unfocusedAlpha,
111
113
  focusedAlpha,
112
114
  rebuildPaths,
113
- focusTargets: focus?.targets
115
+ focusTargets: focusSeries2?.targets
114
116
  });
115
117
  }
116
118
  });
@@ -231,11 +233,11 @@ var legend = (Component, options = {}) => {
231
233
  var _tmpl$2 = /* @__PURE__ */ template(`<div role=tooltip aria-label="Chart tooltip">`);
232
234
  var TOOLTIP_OFFSET_X = 8;
233
235
  var TOOLTIP_OFFSET_Y = 8;
234
- var getTooltipPosition = (placement, left, top, tooltipWidth, tooltipHeight) => {
236
+ var getTooltipPosition = (placement, left, top, tooltipWidth, tooltipHeight, isFixed = false) => {
235
237
  const baseX = placement.includes("left") ? left - tooltipWidth - TOOLTIP_OFFSET_X : left + TOOLTIP_OFFSET_X;
236
238
  const baseY = placement.includes("top") ? top - tooltipHeight - TOOLTIP_OFFSET_Y : top + TOOLTIP_OFFSET_Y;
237
- const viewportX = baseX - window.scrollX;
238
- const viewportY = baseY - window.scrollY;
239
+ const viewportX = isFixed ? baseX : baseX - window.scrollX;
240
+ const viewportY = isFixed ? baseY : baseY - window.scrollY;
239
241
  const overflowsLeft = viewportX < 0;
240
242
  const overflowsRight = viewportX + tooltipWidth > window.innerWidth;
241
243
  const overflowsTop = viewportY < 0;
@@ -271,12 +273,13 @@ var tooltip = (Component, options = {}) => {
271
273
  const TooltipRoot = () => {
272
274
  const _options = mergeProps({
273
275
  placement: "top-left",
276
+ fixed: false,
274
277
  id: "solid-uplot-tooltip-root",
275
278
  style: {},
276
279
  zIndex: 20
277
280
  }, options);
278
281
  const chartCursorData = () => bus.data.cursor?.state[u.root.id];
279
- const [tooltipOptions, containerProps] = splitProps(_options, ["placement"]);
282
+ const [tooltipOptions, containerProps] = splitProps(_options, ["placement", "fixed", "onPositionCalculated"]);
280
283
  return createComponent(Show, {
281
284
  get when() {
282
285
  return chartCursorData();
@@ -284,11 +287,14 @@ var tooltip = (Component, options = {}) => {
284
287
  children: (cursor2) => {
285
288
  const position = () => {
286
289
  const overRect = u.over.getBoundingClientRect();
287
- const absoluteLeft = overRect.left + cursor2().position.left + window.scrollX;
288
- const absoluteTop = overRect.top + cursor2().position.top + window.scrollY;
289
290
  const tooltipWidth = tooltipRoot.offsetWidth ?? 0;
290
291
  const tooltipHeight = tooltipRoot.offsetHeight ?? 0;
291
- return getTooltipPosition(tooltipOptions.placement, absoluteLeft, absoluteTop, tooltipWidth, tooltipHeight);
292
+ const cursorLeft = overRect.left + cursor2().position.left;
293
+ const cursorTop = overRect.top + cursor2().position.top;
294
+ const absoluteLeft = tooltipOptions.fixed ? cursorLeft : cursorLeft + window.scrollX;
295
+ const absoluteTop = tooltipOptions.fixed ? cursorTop : cursorTop + window.scrollY;
296
+ const calculatedPosition = getTooltipPosition(tooltipOptions.placement, absoluteLeft, absoluteTop, tooltipWidth, tooltipHeight, tooltipOptions.fixed);
297
+ return tooltipOptions.onPositionCalculated ? tooltipOptions.onPositionCalculated(calculatedPosition, tooltipOptions.placement) : calculatedPosition;
292
298
  };
293
299
  return (() => {
294
300
  var _el$ = _tmpl$2();
@@ -306,11 +312,11 @@ var tooltip = (Component, options = {}) => {
306
312
  }));
307
313
  effect((_p$) => {
308
314
  var _v$ = containerProps.id, _v$2 = containerProps.class, _v$3 = {
309
- position: "absolute",
315
+ position: tooltipOptions.fixed ? "fixed" : "absolute",
310
316
  "z-index": containerProps.zIndex,
317
+ "pointer-events": "none",
311
318
  left: `${position().left}px`,
312
319
  top: `${position().top}px`,
313
- "pointer-events": "none",
314
320
  ...containerProps.style
315
321
  };
316
322
  _v$ !== _p$.e && setAttribute(_el$, "id", _p$.e = _v$);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dschz/solid-uplot",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "description": "SolidJS wrapper for uPlot — ultra-fast, tiny time-series & charting library",
5
5
  "type": "module",
6
6
  "author": "Daniel Sanchez <dsanc89@pm.me>",
@@ -59,14 +59,12 @@
59
59
  "browser": {},
60
60
  "exports": {
61
61
  ".": {
62
- "solid": "./dist/index/index.jsx",
63
62
  "import": {
64
63
  "types": "./dist/index/index.d.ts",
65
64
  "default": "./dist/index/index.js"
66
65
  }
67
66
  },
68
67
  "./plugins": {
69
- "solid": "./dist/plugins/index.jsx",
70
68
  "import": {
71
69
  "types": "./dist/plugins/index.d.ts",
72
70
  "default": "./dist/plugins/index.js"
@@ -107,46 +105,46 @@
107
105
  "typecheck": "tsc --noEmit"
108
106
  },
109
107
  "devDependencies": {
110
- "@changesets/cli": "^2.29.3",
111
- "@dschz/solid-auto-sizer": "^0.1.0",
112
- "@solidjs/router": "^0.15.3",
108
+ "@changesets/cli": "^2.29.7",
109
+ "@dschz/solid-auto-sizer": "^0.1.3",
110
+ "@solidjs/router": "^0.15.4",
113
111
  "@solidjs/testing-library": "^0.8.10",
114
- "@tailwindcss/vite": "^4.1.5",
115
- "@testing-library/jest-dom": "^6.6.3",
112
+ "@tailwindcss/vite": "^4.1.17",
113
+ "@testing-library/jest-dom": "^6.9.1",
116
114
  "@testing-library/user-event": "^14.6.1",
117
- "@types/bun": "^1.2.12",
118
- "@types/prismjs": "^1.26.3",
119
- "@typescript-eslint/eslint-plugin": "^8.32.0",
120
- "@typescript-eslint/parser": "^8.32.0",
121
- "@vitest/coverage-istanbul": "^3.1.3",
115
+ "@types/bun": "^1.3.3",
116
+ "@types/prismjs": "^1.26.5",
117
+ "@typescript-eslint/eslint-plugin": "^8.47.0",
118
+ "@typescript-eslint/parser": "^8.47.0",
119
+ "@vitest/coverage-istanbul": "^4.0.13",
122
120
  "@wessberg/pointer-events": "^1.0.9",
123
- "canvas": "^3.1.0",
124
- "eslint": "^9.26.0",
121
+ "canvas": "^3.2.0",
122
+ "eslint": "^9.39.1",
125
123
  "eslint-plugin-simple-import-sort": "^12.1.1",
126
124
  "eslint-plugin-solid": "^0.14.5",
127
- "globals": "^16.1.0",
128
- "jiti": "^2.4.2",
129
- "jsdom": "^26.1.0",
130
- "path2d": "^0.2.2",
131
- "prettier": "^3.5.3",
132
- "prismjs": "^1.29.0",
133
- "solid-js": "^1.9.6",
134
- "solid-prism-editor": "^2.0.0",
135
- "tailwindcss": "^4.1.5",
136
- "tsup": "^8.4.0",
125
+ "globals": "^16.5.0",
126
+ "jiti": "^2.6.1",
127
+ "jsdom": "^27.2.0",
128
+ "path2d": "^0.3.1",
129
+ "prettier": "^3.6.2",
130
+ "prismjs": "^1.30.0",
131
+ "solid-js": "^1.9.10",
132
+ "solid-prism-editor": "^2.1.0",
133
+ "tailwindcss": "^4.1.17",
134
+ "tsup": "^8.5.1",
137
135
  "tsup-preset-solid": "^2.2.0",
138
- "typescript": "^5.8.3",
139
- "typescript-eslint": "^8.32.0",
136
+ "typescript": "^5.9.3",
137
+ "typescript-eslint": "^8.47.0",
140
138
  "uplot": "^1.6.32",
141
- "vite": "^6.3.5",
142
- "vite-plugin-solid": "^2.11.6",
143
- "vitest": "^3.1.3"
139
+ "vite": "^7.2.4",
140
+ "vite-plugin-solid": "^2.11.10",
141
+ "vitest": "^4.0.13"
144
142
  },
145
143
  "peerDependencies": {
146
144
  "solid-js": ">=1.6.0",
147
145
  "uplot": ">=1.6.32"
148
146
  },
149
147
  "dependencies": {
150
- "@solid-primitives/refs": "^1.1.1"
148
+ "@solid-primitives/refs": "^1.1.2"
151
149
  }
152
150
  }
@@ -1,42 +0,0 @@
1
- // src/utils/getCursorData.ts
2
- var getCursorData = (u) => {
3
- const idx = u.cursor.idx;
4
- const xValues = u.data[0];
5
- const isValid = idx != null && xValues && idx < xValues.length;
6
- return !isValid ? void 0 : {
7
- plotId: u.root.id,
8
- idx,
9
- xValue: xValues[idx],
10
- visible: Boolean(u.cursor.show),
11
- position: { left: u.cursor.left || 0, top: u.cursor.top || 0 }
12
- };
13
- };
14
-
15
- // src/utils/getSeriesData.ts
16
- var getSeriesData = (u, options = {}) => {
17
- const series = [];
18
- for (let i = 1; i < u.series.length; i++) {
19
- const s = u.series[i];
20
- const stroke = typeof s.stroke === "function" ? s.stroke(u, i) : s.stroke;
21
- const fill = typeof s.fill === "function" ? s.fill(u, i) : s.fill;
22
- const label = options.labelTransform?.(s.label) || s.label?.toString() || `Series ${i}`;
23
- series.push({
24
- idx: i - 1,
25
- seriesIdx: i,
26
- label,
27
- stroke: stroke ?? "#000",
28
- fill: fill ?? "transparent",
29
- width: s.width,
30
- dash: s.dash,
31
- scale: s.scale,
32
- visible: Boolean(s.show)
33
- });
34
- }
35
- return series;
36
- };
37
-
38
- export {
39
- getCursorData,
40
- getSeriesData
41
- };
42
- // istanbul ignore next -- @preserve
@@ -1,160 +0,0 @@
1
- import {
2
- getCursorData,
3
- getSeriesData
4
- } from "../chunk/X3NAOFDO.jsx";
5
-
6
- // src/createPluginBus.tsx
7
- import { createStore } from "solid-js/store";
8
- var createPluginBus = (initialData = {}) => {
9
- const [data, setData] = createStore(initialData);
10
- return { data, setData };
11
- };
12
-
13
- // src/SolidUplot.tsx
14
- import "uplot/dist/uPlot.min.css";
15
- import { mergeRefs } from "@solid-primitives/refs";
16
- import {
17
- createEffect,
18
- createMemo,
19
- createUniqueId,
20
- mergeProps,
21
- onCleanup,
22
- splitProps,
23
- untrack
24
- } from "solid-js";
25
- import uPlot2 from "uplot";
26
-
27
- // src/utils/getNewCalendarDayIndices.ts
28
- import "uplot";
29
-
30
- // src/eventPlugins.ts
31
- var createCursorMovePlugin = (onCursorMove) => {
32
- return {
33
- hooks: {
34
- setCursor: (u) => {
35
- const cursor = getCursorData(u);
36
- if (!cursor) return;
37
- onCursorMove?.({ u, cursor, seriesData: getSeriesData(u) });
38
- }
39
- }
40
- };
41
- };
42
-
43
- // src/SolidUplot.tsx
44
- var SolidUplot = (props) => {
45
- let container;
46
- const _props = mergeProps(
47
- {
48
- id: createUniqueId(),
49
- childrenPlacement: "top",
50
- width: 600,
51
- height: 300,
52
- autoResize: false,
53
- data: [],
54
- resetScales: true,
55
- plugins: [],
56
- legend: {
57
- show: false
58
- }
59
- },
60
- props
61
- );
62
- const [local, options] = splitProps(_props, [
63
- "children",
64
- "childrenPlacement",
65
- "class",
66
- "autoResize",
67
- "onCreate",
68
- "onCursorMove",
69
- "style",
70
- "ref"
71
- ]);
72
- const [updateableOptions, newChartOptions] = splitProps(options, [
73
- "data",
74
- "width",
75
- "height",
76
- "resetScales"
77
- ]);
78
- const [system, chartOptions] = splitProps(newChartOptions, ["pluginBus", "plugins"]);
79
- const size = () => ({ width: updateableOptions.width, height: updateableOptions.height });
80
- const chartPlugins = createMemo(() => {
81
- const plugins = system.plugins.map(
82
- (plugin) => typeof plugin === "function" ? plugin({ bus: system.pluginBus }) : plugin
83
- );
84
- if (local.onCursorMove) {
85
- plugins.push(createCursorMovePlugin(local.onCursorMove));
86
- }
87
- return plugins;
88
- });
89
- createEffect(() => {
90
- const getInitialSize = () => {
91
- if (local.autoResize) {
92
- const rect = container.getBoundingClientRect();
93
- return {
94
- width: rect.width > 0 ? Math.floor(rect.width) : 600,
95
- height: rect.height > 0 ? Math.floor(rect.height) : 300
96
- };
97
- }
98
- return untrack(size);
99
- };
100
- const initialSize = getInitialSize();
101
- const initialData = untrack(() => updateableOptions.data);
102
- const chart = new uPlot2(
103
- {
104
- ...chartOptions,
105
- ...initialSize,
106
- plugins: chartPlugins()
107
- },
108
- initialData,
109
- container
110
- );
111
- local.onCreate?.(chart, { seriesData: getSeriesData(chart) });
112
- createEffect(() => {
113
- if (local.autoResize) return;
114
- chart.setSize(size());
115
- });
116
- createEffect(() => {
117
- if (!local.autoResize) return;
118
- const resizeObserver = new ResizeObserver((entries) => {
119
- for (const entry of entries) {
120
- const { width, height } = entry.contentRect;
121
- chart.setSize({ width: Math.floor(width), height: Math.floor(height) });
122
- }
123
- });
124
- resizeObserver.observe(container);
125
- onCleanup(() => {
126
- resizeObserver.disconnect();
127
- });
128
- });
129
- createEffect(() => {
130
- chart.setData(updateableOptions.data, updateableOptions.resetScales);
131
- });
132
- onCleanup(() => {
133
- chart.destroy();
134
- });
135
- });
136
- const classes = () => local.class ? `solid-uplot ${local.class}` : "solid-uplot";
137
- return <div
138
- id="solid-uplot-root"
139
- ref={mergeRefs(local.ref, (el) => container = el)}
140
- class={classes()}
141
- style={{
142
- display: "flex",
143
- "flex-direction": local.childrenPlacement === "top" ? "column" : "column-reverse",
144
- // When autoResize is enabled, fill the parent container
145
- ...local.autoResize && {
146
- width: "100%",
147
- height: "100%",
148
- "min-width": "0",
149
- "min-height": "0"
150
- },
151
- ...local.style
152
- }}
153
- >
154
- {local.children}
155
- </div>;
156
- };
157
- export {
158
- SolidUplot,
159
- createPluginBus
160
- };
@@ -1,334 +0,0 @@
1
- import {
2
- getCursorData,
3
- getSeriesData
4
- } from "../chunk/X3NAOFDO.jsx";
5
-
6
- // src/plugins/cursor.ts
7
- var cursor = () => {
8
- return ({ bus }) => {
9
- if (!bus) {
10
- return { hooks: {} };
11
- }
12
- bus.setData("cursor", {
13
- state: {}
14
- });
15
- let pointerEnter;
16
- let pointerLeave;
17
- return {
18
- hooks: {
19
- ready: (u) => {
20
- pointerEnter = () => {
21
- bus.setData("cursor", { sourceId: u.root.id });
22
- };
23
- pointerLeave = () => {
24
- bus.setData("cursor", { sourceId: void 0 });
25
- };
26
- u.over.addEventListener("pointerenter", pointerEnter);
27
- u.over.addEventListener("pointerleave", pointerLeave);
28
- },
29
- setCursor: (u) => {
30
- bus.setData("cursor", "state", u.root.id, getCursorData(u));
31
- },
32
- setData: (u) => {
33
- bus.setData("cursor", (prev) => ({
34
- ...prev ?? {},
35
- state: {
36
- ...prev?.state ?? {},
37
- [u.root.id]: getCursorData(u)
38
- }
39
- }));
40
- },
41
- destroy: (u) => {
42
- bus.setData("cursor", "state", u.root.id, void 0);
43
- u.over.removeEventListener("pointerenter", pointerEnter);
44
- u.over.removeEventListener("pointerleave", pointerLeave);
45
- }
46
- }
47
- };
48
- };
49
- };
50
-
51
- // src/plugins/focusSeries.ts
52
- import { createEffect, createRoot } from "solid-js";
53
- var DEFAULT_UNFOCUSED_ALPHA = 0.1;
54
- var DEFAULT_FOCUSED_ALPHA = 1;
55
- var DEFAULT_REBUILD_PATHS = false;
56
- var seriesFocusRedraw = (u, options = {}) => {
57
- const {
58
- unfocusedAlpha = DEFAULT_UNFOCUSED_ALPHA,
59
- focusedAlpha = DEFAULT_FOCUSED_ALPHA,
60
- rebuildPaths = DEFAULT_REBUILD_PATHS,
61
- focusTargets
62
- } = options;
63
- for (let i = 1; i < u.series.length; i++) {
64
- const s = u.series[i];
65
- if (!focusTargets || !focusTargets.length) {
66
- s.alpha = 1;
67
- continue;
68
- }
69
- const target = focusTargets.find((t) => {
70
- if ("label" in t) return t.label === s.label;
71
- if ("zeroIndex" in t) return t.zeroIndex === i - 1;
72
- if ("index" in t) return t.index === i;
73
- });
74
- s.alpha = target ? focusedAlpha : unfocusedAlpha;
75
- }
76
- u.redraw(rebuildPaths);
77
- };
78
- var focusSeries = (options = {}) => {
79
- return ({ bus }) => {
80
- if (!bus) {
81
- return { hooks: {} };
82
- }
83
- const {
84
- pxThreshold = 5,
85
- unfocusedAlpha = DEFAULT_UNFOCUSED_ALPHA,
86
- focusedAlpha = DEFAULT_FOCUSED_ALPHA,
87
- rebuildPaths = DEFAULT_REBUILD_PATHS
88
- } = options;
89
- let dispose;
90
- let pointerLeave;
91
- return {
92
- hooks: {
93
- ready: (u) => {
94
- pointerLeave = () => {
95
- bus.setData("focusSeries", void 0);
96
- };
97
- queueMicrotask(() => {
98
- if (bus.data.focusSeries) {
99
- seriesFocusRedraw(u, {
100
- unfocusedAlpha,
101
- focusedAlpha,
102
- rebuildPaths,
103
- focusTargets: bus.data.focusSeries.targets
104
- });
105
- }
106
- });
107
- u.over.addEventListener("pointerleave", pointerLeave);
108
- dispose = createRoot((dispose2) => {
109
- createEffect(() => {
110
- const cursor2 = bus.data.cursor;
111
- const focus = bus.data.focusSeries;
112
- if (cursor2?.sourceId !== u.root.id) {
113
- seriesFocusRedraw(u, {
114
- unfocusedAlpha,
115
- focusedAlpha,
116
- rebuildPaths,
117
- focusTargets: focus?.targets
118
- });
119
- }
120
- });
121
- return dispose2;
122
- });
123
- },
124
- setCursor: (u) => {
125
- const cursor2 = bus.data.cursor;
126
- const chartCursor = cursor2?.state[u.root.id];
127
- if (!cursor2 || !chartCursor || cursor2.sourceId !== u.root.id) return;
128
- const focusTargets = [];
129
- for (let i = 1; i < u.series.length; i++) {
130
- const s = u.series[i];
131
- const yVals = u.data[i];
132
- const val = yVals?.[chartCursor.idx];
133
- if (!s.show || !yVals || val == null) continue;
134
- const yPos = u.valToPos(val, s.scale);
135
- const dist = Math.abs(yPos - chartCursor.position.top);
136
- if (dist <= pxThreshold) {
137
- if (s.label != null) {
138
- focusTargets.push({ label: s.label });
139
- } else {
140
- focusTargets.push({ index: i });
141
- }
142
- }
143
- }
144
- seriesFocusRedraw(u, {
145
- unfocusedAlpha,
146
- focusedAlpha,
147
- rebuildPaths,
148
- focusTargets
149
- });
150
- bus.setData("focusSeries", {
151
- sourceId: u.root.id,
152
- targets: focusTargets
153
- });
154
- },
155
- destroy: (u) => {
156
- dispose();
157
- u.over.removeEventListener("pointerleave", pointerLeave);
158
- }
159
- }
160
- };
161
- };
162
- };
163
-
164
- // src/plugins/legend.tsx
165
- import { mergeProps, splitProps } from "solid-js";
166
- import { render } from "solid-js/web";
167
- import "uplot";
168
- var legend = (Component, options = {}) => {
169
- return ({ bus }) => {
170
- if (!bus) {
171
- return { hooks: {} };
172
- }
173
- let legendRoot;
174
- let dispose;
175
- return {
176
- hooks: {
177
- ready: (u) => {
178
- const seriesData = getSeriesData(u);
179
- const LegendRoot = () => {
180
- const _options = mergeProps(
181
- {
182
- placement: "top-left",
183
- pxOffset: 8,
184
- id: "solid-uplot-legend-root",
185
- zIndex: 10
186
- },
187
- options
188
- );
189
- const [legendOptions, containerProps] = splitProps(_options, ["placement", "pxOffset"]);
190
- const containerStyle = () => {
191
- const overRect = u.over.getBoundingClientRect();
192
- const offset = legendOptions.pxOffset;
193
- return {
194
- position: "absolute",
195
- [legendOptions.placement === "top-left" ? "left" : "right"]: `${offset}px`,
196
- top: `${offset}px`,
197
- "max-width": `${overRect.width - offset * 2}px`,
198
- "max-height": `${overRect.height - offset * 2}px`,
199
- "z-index": containerProps.zIndex,
200
- "pointer-events": "auto",
201
- overflow: "auto",
202
- ...containerProps.style
203
- };
204
- };
205
- return <div
206
- ref={legendRoot}
207
- id={containerProps.id}
208
- class={containerProps.class}
209
- role="group"
210
- aria-label="Chart legend"
211
- style={containerStyle()}
212
- >
213
- <Component u={u} seriesData={seriesData} bus={bus} />
214
- </div>;
215
- };
216
- dispose = render(() => <LegendRoot />, u.over);
217
- },
218
- destroy: () => {
219
- dispose();
220
- legendRoot?.remove();
221
- }
222
- }
223
- };
224
- };
225
- };
226
-
227
- // src/plugins/tooltip.tsx
228
- import { mergeProps as mergeProps2, Show, splitProps as splitProps2 } from "solid-js";
229
- import { render as render2 } from "solid-js/web";
230
- import "uplot";
231
- var TOOLTIP_OFFSET_X = 8;
232
- var TOOLTIP_OFFSET_Y = 8;
233
- var getTooltipPosition = (placement, left, top, tooltipWidth, tooltipHeight) => {
234
- const baseX = placement.includes("left") ? left - tooltipWidth - TOOLTIP_OFFSET_X : left + TOOLTIP_OFFSET_X;
235
- const baseY = placement.includes("top") ? top - tooltipHeight - TOOLTIP_OFFSET_Y : top + TOOLTIP_OFFSET_Y;
236
- const viewportX = baseX - window.scrollX;
237
- const viewportY = baseY - window.scrollY;
238
- const overflowsLeft = viewportX < 0;
239
- const overflowsRight = viewportX + tooltipWidth > window.innerWidth;
240
- const overflowsTop = viewportY < 0;
241
- const overflowsBottom = viewportY + tooltipHeight > window.innerHeight;
242
- let flipX = false;
243
- let flipY = false;
244
- if (placement.includes("left") && overflowsLeft) flipX = true;
245
- if (placement.includes("right") && overflowsRight) flipX = true;
246
- if (placement.includes("top") && overflowsTop) flipY = true;
247
- if (placement.includes("bottom") && overflowsBottom) flipY = true;
248
- const finalX = flipX && placement.includes("left") ? left + TOOLTIP_OFFSET_X : flipX && placement.includes("right") ? left - tooltipWidth - TOOLTIP_OFFSET_X : baseX;
249
- const finalY = flipY && placement.includes("top") ? top + TOOLTIP_OFFSET_Y : flipY && placement.includes("bottom") ? top - tooltipHeight - TOOLTIP_OFFSET_Y : baseY;
250
- return {
251
- left: finalX,
252
- top: finalY
253
- };
254
- };
255
- var tooltip = (Component, options = {}) => {
256
- return ({ bus }) => {
257
- if (!bus) {
258
- return { hooks: {} };
259
- }
260
- let tooltipRoot;
261
- let dispose;
262
- return {
263
- hooks: {
264
- ready: (u) => {
265
- const seriesData = getSeriesData(u);
266
- const TooltipRoot = () => {
267
- const _options = mergeProps2(
268
- {
269
- placement: "top-left",
270
- id: "solid-uplot-tooltip-root",
271
- style: {},
272
- zIndex: 20
273
- },
274
- options
275
- );
276
- const chartCursorData = () => bus.data.cursor?.state[u.root.id];
277
- const [tooltipOptions, containerProps] = splitProps2(_options, ["placement"]);
278
- return <Show when={chartCursorData()}>
279
- {(cursor2) => {
280
- const position = () => {
281
- const overRect = u.over.getBoundingClientRect();
282
- const absoluteLeft = overRect.left + cursor2().position.left + window.scrollX;
283
- const absoluteTop = overRect.top + cursor2().position.top + window.scrollY;
284
- const tooltipWidth = tooltipRoot.offsetWidth ?? 0;
285
- const tooltipHeight = tooltipRoot.offsetHeight ?? 0;
286
- return getTooltipPosition(
287
- tooltipOptions.placement,
288
- absoluteLeft,
289
- absoluteTop,
290
- tooltipWidth,
291
- tooltipHeight
292
- );
293
- };
294
- return <div
295
- ref={tooltipRoot}
296
- id={containerProps.id}
297
- class={containerProps.class}
298
- role="tooltip"
299
- aria-label="Chart tooltip"
300
- style={{
301
- position: "absolute",
302
- "z-index": containerProps.zIndex,
303
- left: `${position().left}px`,
304
- top: `${position().top}px`,
305
- "pointer-events": "none",
306
- ...containerProps.style
307
- }}
308
- >
309
- <Component
310
- u={u}
311
- seriesData={seriesData}
312
- cursor={cursor2()}
313
- focusedSeries={bus.data.focusSeries}
314
- />
315
- </div>;
316
- }}
317
- </Show>;
318
- };
319
- dispose = render2(() => <TooltipRoot />, u.root);
320
- },
321
- destroy: () => {
322
- dispose();
323
- tooltipRoot?.remove();
324
- }
325
- }
326
- };
327
- };
328
- };
329
- export {
330
- cursor,
331
- focusSeries,
332
- legend,
333
- tooltip
334
- };