@dschz/solid-uplot
Version:
SolidJS wrapper for uPlot — ultra-fast, tiny time-series & charting library
355 lines (352 loc) • 13 kB
JavaScript
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 };