UNPKG

@dschz/solid-uplot

Version:

SolidJS wrapper for uPlot — ultra-fast, tiny time-series & charting library

355 lines (352 loc) 13 kB
import { getCursorData, getSeriesData } from '../chunk/QELYARMN.js'; import { createRoot, createEffect, mergeProps, splitProps, Show } from 'solid-js'; import { render, createComponent, use, insert, effect, setAttribute, className, style, template } from 'solid-js/web'; // src/plugins/cursor.ts var cursor = () => { return ({ bus }) => { if (!bus) { return { hooks: {} }; } bus.setData("cursor", { state: {} }); let pointerEnter; let pointerLeave; return { hooks: { ready: (u) => { pointerEnter = () => { bus.setData("cursor", { sourceId: u.root.id }); }; pointerLeave = () => { bus.setData("cursor", { sourceId: void 0 }); }; u.over.addEventListener("pointerenter", pointerEnter); u.over.addEventListener("pointerleave", pointerLeave); }, setCursor: (u) => { bus.setData("cursor", "state", u.root.id, getCursorData(u)); }, setData: (u) => { bus.setData("cursor", (prev) => ({ ...prev ?? {}, state: { ...prev?.state ?? {}, [u.root.id]: getCursorData(u) } })); }, destroy: (u) => { bus.setData("cursor", "state", u.root.id, void 0); u.over.removeEventListener("pointerenter", pointerEnter); u.over.removeEventListener("pointerleave", pointerLeave); } } }; }; }; var DEFAULT_UNFOCUSED_ALPHA = 0.1; var DEFAULT_FOCUSED_ALPHA = 1; var DEFAULT_REBUILD_PATHS = false; var seriesFocusRedraw = (u, options = {}) => { const { unfocusedAlpha = DEFAULT_UNFOCUSED_ALPHA, focusedAlpha = DEFAULT_FOCUSED_ALPHA, rebuildPaths = DEFAULT_REBUILD_PATHS, focusTargets } = options; for (let i = 1; i < u.series.length; i++) { const s = u.series[i]; if (!focusTargets?.length) { s.alpha = 1; continue; } const target = focusTargets.find((t) => { if ("label" in t) return t.label === s.label; if ("zeroIndex" in t) return t.zeroIndex === i - 1; return "index" in t && t.index === i; }); s.alpha = target ? focusedAlpha : unfocusedAlpha; } u.redraw(rebuildPaths); }; var focusSeries = (options = {}) => { return ({ bus }) => { if (!bus) { return { hooks: {} }; } const { pxThreshold = 5, unfocusedAlpha = DEFAULT_UNFOCUSED_ALPHA, focusedAlpha = DEFAULT_FOCUSED_ALPHA, rebuildPaths = DEFAULT_REBUILD_PATHS } = options; let dispose; let pointerLeave; return { hooks: { ready: (u) => { pointerLeave = () => { bus.setData("focusSeries", void 0); }; queueMicrotask(() => { if (bus.data.focusSeries) { seriesFocusRedraw(u, { unfocusedAlpha, focusedAlpha, rebuildPaths, focusTargets: bus.data.focusSeries.targets }); } }); u.over.addEventListener("pointerleave", pointerLeave); dispose = createRoot((dispose2) => { createEffect(() => { const cursor2 = bus.data.cursor; const focusSeries2 = bus.data.focusSeries; const isNotSourceChart = cursor2?.sourceId !== u.root.id; const userRedrawCondition = options.shouldRedrawOnBusUpdate?.({ u, cursor: cursor2, focusSeries: focusSeries2 }) ?? true; if (isNotSourceChart && userRedrawCondition) { seriesFocusRedraw(u, { unfocusedAlpha, focusedAlpha, rebuildPaths, focusTargets: focusSeries2?.targets }); } }); return dispose2; }); }, setCursor: (u) => { const cursor2 = bus.data.cursor; const chartCursor = cursor2?.state[u.root.id]; if (!cursor2 || !chartCursor || cursor2.sourceId !== u.root.id) return; const focusTargets = []; for (let i = 1; i < u.series.length; i++) { const s = u.series[i]; const yVals = u.data[i]; const val = yVals?.[chartCursor.idx]; if (!s.show || !yVals || val == null) continue; const yPos = u.valToPos(val, s.scale); const dist = Math.abs(yPos - chartCursor.position.top); if (dist <= pxThreshold) { if (s.label != null) { focusTargets.push({ label: s.label }); } else { focusTargets.push({ index: i }); } } } seriesFocusRedraw(u, { unfocusedAlpha, focusedAlpha, rebuildPaths, focusTargets }); bus.setData("focusSeries", { sourceId: u.root.id, targets: focusTargets }); }, destroy: (u) => { dispose(); u.over.removeEventListener("pointerleave", pointerLeave); } } }; }; }; var _tmpl$ = /* @__PURE__ */ template(`<div role=group aria-label="Chart legend">`); var legend = (Component, options = {}) => { return ({ bus }) => { if (!bus) { return { hooks: {} }; } let legendRoot; let dispose; return { hooks: { ready: (u) => { const seriesData = getSeriesData(u); const LegendRoot = () => { const _options = mergeProps({ placement: "top-left", pxOffset: 8, id: "solid-uplot-legend-root", zIndex: 10 }, options); const [legendOptions, containerProps] = splitProps(_options, ["placement", "pxOffset"]); const containerStyle = () => { const overRect = u.over.getBoundingClientRect(); const offset = legendOptions.pxOffset; return { position: "absolute", [legendOptions.placement === "top-left" ? "left" : "right"]: `${offset}px`, top: `${offset}px`, "max-width": `${overRect.width - offset * 2}px`, "max-height": `${overRect.height - offset * 2}px`, "z-index": containerProps.zIndex, "pointer-events": "none", overflow: "auto", ...containerProps.style }; }; return (() => { var _el$ = _tmpl$(); var _ref$ = legendRoot; typeof _ref$ === "function" ? use(_ref$, _el$) : legendRoot = _el$; insert(_el$, createComponent(Component, { u, seriesData, bus })); effect((_p$) => { var _v$ = containerProps.id, _v$2 = containerProps.class, _v$3 = containerStyle(); _v$ !== _p$.e && setAttribute(_el$, "id", _p$.e = _v$); _v$2 !== _p$.t && className(_el$, _p$.t = _v$2); _p$.a = style(_el$, _v$3, _p$.a); return _p$; }, { e: void 0, t: void 0, a: void 0 }); return _el$; })(); }; dispose = render(() => createComponent(LegendRoot, {}), u.over); }, destroy: () => { dispose(); legendRoot?.remove(); } } }; }; }; var _tmpl$2 = /* @__PURE__ */ template(`<div role=tooltip aria-label="Chart tooltip"style=pointer-events:none>`); var TOOLTIP_OFFSET_X = 8; var TOOLTIP_OFFSET_Y = 8; var getTooltipPosition = (placement, left, top, tooltipWidth, tooltipHeight, rootOffset) => { const baseX = placement.includes("left") ? left - tooltipWidth - TOOLTIP_OFFSET_X : left + TOOLTIP_OFFSET_X; const baseY = placement.includes("top") ? top - tooltipHeight - TOOLTIP_OFFSET_Y : top + TOOLTIP_OFFSET_Y; const viewportX = baseX + rootOffset.left; const viewportY = baseY + rootOffset.top; const overflowsLeft = viewportX < 0; const overflowsRight = viewportX + tooltipWidth > window.innerWidth; const overflowsTop = viewportY < 0; const overflowsBottom = viewportY + tooltipHeight > window.innerHeight; let flipX = false; let flipY = false; if (placement.includes("left") && overflowsLeft) flipX = true; if (placement.includes("right") && overflowsRight) flipX = true; if (placement.includes("top") && overflowsTop) flipY = true; if (placement.includes("bottom") && overflowsBottom) flipY = true; const finalX = flipX && placement.includes("left") ? left + TOOLTIP_OFFSET_X : flipX && placement.includes("right") ? left - tooltipWidth - TOOLTIP_OFFSET_X : baseX; const finalY = flipY && placement.includes("top") ? top + TOOLTIP_OFFSET_Y : flipY && placement.includes("bottom") ? top - tooltipHeight - TOOLTIP_OFFSET_Y : baseY; return { left: finalX, top: finalY }; }; var tooltip = (Component, options = {}) => { return ({ bus }) => { if (!bus) { return { hooks: {} }; } let tooltipRoot; let dispose; return { hooks: { ready: (u) => { const seriesData = getSeriesData(u); const TooltipRoot = () => { const _options = mergeProps({ placement: "top-left", fixed: false, id: "solid-uplot-tooltip-root", style: {}, zIndex: 20 }, options); const chartCursorData = () => bus.data.cursor?.state[u.root.id]; const [tooltipOptions, containerProps] = splitProps(_options, ["placement", "fixed", "onPositionCalculated"]); return createComponent(Show, { get when() { return chartCursorData(); }, children: (cursor2) => { const position = () => { const overRect = u.over.getBoundingClientRect(); const rootRect = u.root.getBoundingClientRect(); const tooltipWidth = tooltipRoot.offsetWidth ?? 0; const tooltipHeight = tooltipRoot.offsetHeight ?? 0; const cursorViewportLeft = overRect.left + cursor2().position.left; const cursorViewportTop = overRect.top + cursor2().position.top; const positionedLeft = tooltipOptions.fixed ? cursorViewportLeft : cursorViewportLeft - rootRect.left; const positionedTop = tooltipOptions.fixed ? cursorViewportTop : cursorViewportTop - rootRect.top; const rootOffset = tooltipOptions.fixed ? { left: 0, top: 0 } : { left: rootRect.left, top: rootRect.top }; const calculatedPosition = getTooltipPosition(tooltipOptions.placement, positionedLeft, positionedTop, tooltipWidth, tooltipHeight, rootOffset); return tooltipOptions.onPositionCalculated ? tooltipOptions.onPositionCalculated(calculatedPosition, tooltipOptions.placement) : calculatedPosition; }; return (() => { var _el$ = _tmpl$2(); var _ref$ = tooltipRoot; typeof _ref$ === "function" ? use(_ref$, _el$) : tooltipRoot = _el$; insert(_el$, createComponent(Component, { u, seriesData, get cursor() { return cursor2(); }, get focusedSeries() { return bus.data.focusSeries; } })); effect((_p$) => { var _v$ = containerProps.id, _v$2 = containerProps.class, _v$3 = { position: tooltipOptions.fixed ? "fixed" : "absolute", "z-index": containerProps.zIndex, left: `${position().left}px`, top: `${position().top}px`, ...containerProps.style }; _v$ !== _p$.e && setAttribute(_el$, "id", _p$.e = _v$); _v$2 !== _p$.t && className(_el$, _p$.t = _v$2); _p$.a = style(_el$, _v$3, _p$.a); return _p$; }, { e: void 0, t: void 0, a: void 0 }); return _el$; })(); } }); }; dispose = render(() => createComponent(TooltipRoot, {}), u.root); }, destroy: () => { dispose(); tooltipRoot?.remove(); } } }; }; }; export { cursor, focusSeries, legend, tooltip };