@dschz/solid-uplot
Version:
SolidJS wrapper for uPlot — ultra-fast, tiny time-series & charting library
335 lines (330 loc) • 10.9 kB
JSX
import {
getCursorData,
getSeriesData
} from "../chunk/X3NAOFDO.jsx";
// 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);
}
}
};
};
};
// src/plugins/focusSeries.ts
import { createEffect, createRoot } from "solid-js";
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 || !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;
if ("index" in t) return 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 focus = bus.data.focusSeries;
if (cursor2?.sourceId !== u.root.id) {
seriesFocusRedraw(u, {
unfocusedAlpha,
focusedAlpha,
rebuildPaths,
focusTargets: focus?.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);
}
}
};
};
};
// src/plugins/legend.tsx
import { mergeProps, splitProps } from "solid-js";
import { render } from "solid-js/web";
import "uplot";
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": "auto",
overflow: "auto",
...containerProps.style
};
};
return <div
ref={legendRoot}
id={containerProps.id}
class={containerProps.class}
role="group"
aria-label="Chart legend"
style={containerStyle()}
>
<Component u={u} seriesData={seriesData} bus={bus} />
</div>;
};
dispose = render(() => <LegendRoot />, u.over);
},
destroy: () => {
dispose();
legendRoot?.remove();
}
}
};
};
};
// src/plugins/tooltip.tsx
import { mergeProps as mergeProps2, Show, splitProps as splitProps2 } from "solid-js";
import { render as render2 } from "solid-js/web";
import "uplot";
var TOOLTIP_OFFSET_X = 8;
var TOOLTIP_OFFSET_Y = 8;
var getTooltipPosition = (placement, left, top, tooltipWidth, tooltipHeight) => {
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 - window.scrollX;
const viewportY = baseY - window.scrollY;
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 = mergeProps2(
{
placement: "top-left",
id: "solid-uplot-tooltip-root",
style: {},
zIndex: 20
},
options
);
const chartCursorData = () => bus.data.cursor?.state[u.root.id];
const [tooltipOptions, containerProps] = splitProps2(_options, ["placement"]);
return <Show when={chartCursorData()}>
{(cursor2) => {
const position = () => {
const overRect = u.over.getBoundingClientRect();
const absoluteLeft = overRect.left + cursor2().position.left + window.scrollX;
const absoluteTop = overRect.top + cursor2().position.top + window.scrollY;
const tooltipWidth = tooltipRoot.offsetWidth ?? 0;
const tooltipHeight = tooltipRoot.offsetHeight ?? 0;
return getTooltipPosition(
tooltipOptions.placement,
absoluteLeft,
absoluteTop,
tooltipWidth,
tooltipHeight
);
};
return <div
ref={tooltipRoot}
id={containerProps.id}
class={containerProps.class}
role="tooltip"
aria-label="Chart tooltip"
style={{
position: "absolute",
"z-index": containerProps.zIndex,
left: `${position().left}px`,
top: `${position().top}px`,
"pointer-events": "none",
...containerProps.style
}}
>
<Component
u={u}
seriesData={seriesData}
cursor={cursor2()}
focusedSeries={bus.data.focusSeries}
/>
</div>;
}}
</Show>;
};
dispose = render2(() => <TooltipRoot />, u.root);
},
destroy: () => {
dispose();
tooltipRoot?.remove();
}
}
};
};
};
export {
cursor,
focusSeries,
legend,
tooltip
};