License uPlot npm Bundle Size JSR CI Discord
💹 SolidJS wrapper for uPlot — an ultra-fast, small footprint charting library for time-series data.
- ✅ Fully reactive SolidJS wrapper around uPlot
- 🔌 Plugin system support with inter-plugin communication
- 🎯 Fine-grained control over chart lifecycle
- 💡 Lightweight and fast
- 💻 TypeScript support out of the box
- 🎨 Built-in plugins for tooltips, legends, cursor tracking, and series focusing
- 📱 Responsive sizing support with auto-resize capabilities
npm install solid-js uplot @dschz/solid-uplot pnpm install solid-js uplot @dschz/solid-uplot yarn install solid-js uplot @dschz/solid-uplot bun install solid-js uplot @dschz/solid-uplot
This repo ships with a live, running playground — a full showcase of every feature in the library. It's the fastest way to see solid-uplot in action and explore real, working examples of charts, plugins, responsive sizing, and external integrations.
git clone https://github.com/dsnchz/solid-uplot.git cd solid-uplot # Install dependencies bun install # Start the playground bun dev
The playground runs at http://localhost:3000 and includes interactive demos for:
- All built-in plugins (cursor, tooltip, legend, focus series)
- Responsive and auto-resize chart patterns
- Custom plugin development
- External component integration via the plugin bus
This package provides three main export paths for different functionality:
Core components and plugin system:
import { SolidUplot, createPluginBus } from "@dschz/solid-uplot"; import type { SolidUplotPluginBus, UplotPluginFactory, UplotPluginFactoryContext, } from "@dschz/solid-uplot";
This export path provides four plugins (three of which can be considered primitives).
cursor: transmits cursor position datafocusSeries: transmits which series are visually emphasizedtooltip: plugin that allows you to present a custom JSX tooltip around the cursorlegend: plugin that allows you to present a custom JSX component as your legend over the canvas drawing area.
import { cursor, tooltip, legend, focusSeries } from "@dschz/solid-uplot/plugins"; import type { CursorPluginMessageBus, FocusSeriesPluginMessageBus, TooltipProps, LegendProps, } from "@dschz/solid-uplot/plugins";
Some convenience utility functions for getting certain bits of data from a uPlot instance (except for getColorString which translates a series' stroke or fill into a color value).
import { getSeriesData, getCursorData, getColorString, getNewCalendarDayIndices, } from "@dschz/solid-uplot/utils"; import type { SeriesDatum, CursorData } from "@dschz/solid-uplot/utils";
import { SolidUplot, createPluginBus } from "@dschz/solid-uplot"; import { cursor, tooltip, legend } from "@dschz/solid-uplot/plugins"; import type { CursorPluginMessageBus, TooltipProps, LegendProps } from "@dschz/solid-uplot/plugins"; // Create a tooltip component const MyTooltip = (props: TooltipProps) => ( <div style={{ background: "white", padding: "8px", border: "1px solid #ccc" }}> <div>X: {props.cursor.xValue}</div> <For each={props.seriesData}> {(series) => { const value = props.u.data[series.seriesIdx]?.[props.cursor.idx]; return ( <div> {series.label}: {value} </div> ); }} </For> </div> ); // Create a legend component const MyLegend = (props: LegendProps) => ( <div style={{ background: "rgba(255,255,255,0.9)", padding: "8px" }}> <For each={props.seriesData}> {(series) => ( <div style={{ display: "flex", "align-items": "center", gap: "4px" }}> <div style={{ width: "12px", height: "12px", background: series.stroke, }} /> <span>{series.label}</span> </div> )} </For> </div> ); const MyChart = () => { const bus = createPluginBus<CursorPluginMessageBus>(); return ( <SolidUplot data={[ [0, 1, 2, 3], // x values [10, 20, 30, 40], // y values for series 1 [15, 25, 35, 45], // y values for series 2 ]} width={600} height={400} series={[ {}, { label: "Series 1", stroke: "#1f77b4" }, { label: "Series 2", stroke: "#ff7f0e" }, ]} plugins={[ cursor(), tooltip(MyTooltip), legend(MyLegend, { placement: "top-right", pxOffset: 12 }), ]} pluginBus={bus} /> ); };
For responsive charts that automatically adapt to container size changes, use the autoResize prop:
<div style={{ width: "100%", height: "400px" }}> <SolidUplot autoResize={true} data={data} series={series} // Chart will automatically resize to fill the container /> </div>
For more advanced responsive patterns, you can pair this library with @dschz/solid-auto-sizer:
npm install @dschz/solid-auto-sizer pnpm install @dschz/solid-auto-sizer yarn install @dschz/solid-auto-sizer bun install @dschz/solid-auto-sizer
import { AutoSizer } from "@dschz/solid-auto-sizer"; <AutoSizer> {({ width, height }) => <SolidUplot width={width} height={height} data={data} />} </AutoSizer>;
Alternatively, you can use createElementSize from @solid-primitives/resize-observer for a signal-based approach:
npm install @solid-primitives/resize-observer
import { createElementSize } from "@solid-primitives/resize-observer"; const ResponsiveChart = () => { let container!: HTMLDivElement; const size = createElementSize(() => container); return ( <div style={{ width: "100%", height: "400px" }}> <div ref={container} style={{ width: "100%", height: "100%" }}> <SolidUplot data={data} width={size.width ?? 0} height={size.height ?? 0} series={series} /> </div> </div> ); };
The cornerstone feature of SolidUplot is its refined plugin system that enables extensible functionality and inter-plugin communication through a reactive message bus.
The Plugin Bus System enables plugins to communicate with each other and external components through a reactive store. This architecture provides:
- Type-safe communication: All plugin messages are fully typed
- Reactive updates: Changes in plugin state automatically trigger updates
- Decoupled components: Plugins can interact without direct dependencies
- Extensible: Easy to add new plugins that integrate with existing ones
Tracks cursor position and interaction state across charts:
import { cursor } from "@dschz/solid-uplot/plugins"; import type { CursorPluginMessageBus } from "@dschz/solid-uplot/plugins"; const cursorPlugin = cursor();
The cursor plugin provides cursor position data that other plugins can consume through the bus.
Highlights series based on cursor proximity:
import { focusSeries } from "@dschz/solid-uplot/plugins"; import type { FocusSeriesPluginMessageBus } from "@dschz/solid-uplot/plugins"; const focusPlugin = focusSeries({ pxThreshold: 15, // Distance threshold for focusing (default: 15) });
Renders custom tooltips with automatic positioning and overflow handling:
import { tooltip } from "@dschz/solid-uplot/plugins"; import type { TooltipProps } from "@dschz/solid-uplot/plugins"; const MyTooltip: Component<TooltipProps> = (props) => { return ( <div style={{ background: "white", border: "1px solid #ccc", padding: "8px", "border-radius": "4px", "box-shadow": "0 2px 4px rgba(0,0,0,0.1)", }} > <div style={{ "font-weight": "bold", "margin-bottom": "8px" }}>X: {props.cursor.xValue}</div> <For each={props.seriesData}> {(series) => { const value = () => props.u.data[series.seriesIdx]?.[props.cursor.idx]; return ( <Show when={series.visible}> <div style={{ display: "flex", "align-items": "center", "margin-bottom": "4px" }}> <div style={{ width: "10px", height: "10px", "border-radius": "50%", "background-color": series.stroke, "margin-right": "6px", }} /> <span style={{ color: series.stroke }}> {series.label}: {value()?.toFixed(2)} </span> </div> </Show> ); }} </For> </div> ); }; const tooltipPlugin = tooltip(MyTooltip, { placement: "top-left", // "top-left" | "top-right" | "bottom-left" | "bottom-right" zIndex: 20, });
Adds customizable legends with smart positioning and interactive features:
import { legend } from "@dschz/solid-uplot/plugins"; import type { LegendProps } from "@dschz/solid-uplot/plugins"; const MyLegend: Component<LegendProps> = (props) => { // Access cursor data for interactive features const cursorVisible = () => props.bus.data.cursor?.state[props.u.root.id]?.visible; return ( <div style={{ background: "white", border: "1px solid #ddd", "border-radius": "4px", padding: "8px", "box-shadow": "0 2px 4px rgba(0,0,0,0.1)", // Dim when tooltip is active opacity: cursorVisible() ? 0.6 : 1, transition: "opacity 200ms", }} > <div style={{ "font-weight": "bold", "margin-bottom": "8px" }}>Legend</div> <For each={props.seriesData}> {(series) => ( <Show when={series.visible}> <div style={{ display: "flex", "align-items": "center", gap: "6px", "margin-bottom": "4px", }} > <div style={{ width: "12px", height: "12px", "background-color": series.stroke, "border-radius": "2px", }} /> <span style={{ "font-size": "14px" }}>{series.label}</span> </div> </Show> )} </For> </div> ); }; const legendPlugin = legend(MyLegend, { placement: "top-left", // "top-left" | "top-right" pxOffset: 8, // Distance from chart edges (default: 8) zIndex: 10, });
Legend Plugin Features:
- Simple positioning: Only top-left or top-right corners to avoid axis conflicts
- Size-constrained: Legend cannot exceed chart drawing area dimensions
- Layout-agnostic: You control internal layout and styling
- Non-interfering: Designed to work harmoniously with chart interactions
- Plugin bus integration: Access cursor and focus data for smart interactions
- Automatic cleanup: Proper memory management and DOM cleanup
When using multiple plugins, ensure type safety by properly typing the plugin bus:
import { createPluginBus } from "@dschz/solid-uplot"; import type { CursorPluginMessageBus, FocusSeriesPluginMessageBus, } from "@dschz/solid-uplot/plugins"; // Create a bus that includes all plugin message types const bus = createPluginBus<CursorPluginMessageBus & FocusSeriesPluginMessageBus>(); const MyChart = () => { return ( <SolidUplot data={data} pluginBus={bus} plugins={[cursor(), focusSeries(), tooltip(MyTooltip), legend(MyLegend)]} /> ); };
The plugin system is open to extension. When authoring plugins for public consumption, follow this pattern:
import type { UplotPluginFactory } from "@dschz/solid-uplot"; import type { CursorPluginMessageBus } from "@dschz/solid-uplot/plugins"; // 1. Define your plugin's message type export type MyPluginMessage = { value: number; timestamp: number; }; // 2. Define your plugin's message bus export type MyPluginMessageBus = { myPlugin?: MyPluginMessage; }; // 3. Export your plugin factory export const myPlugin = ( options = {}, ): UplotPluginFactory<CursorPluginMessageBus & MyPluginMessageBus> => { return ({ bus }) => { if (!bus) { console.warn("[my-plugin]: A plugin bus is required"); return { hooks: {} }; } return { hooks: { ready: (u) => { // Initialize plugin state bus.setData("myPlugin", { value: 0, timestamp: Date.now(), }); }, setData: (u) => { // Update plugin state bus.setData("myPlugin", "value", (prev) => prev + 1); }, }, }; }; };
The plugin bus enables powerful integrations between charts and external components:
import { createPluginBus } from "@dschz/solid-uplot"; import type { FocusSeriesPluginMessageBus } from "@dschz/solid-uplot/plugins"; const bus = createPluginBus<FocusSeriesPluginMessageBus>(); // External component that can trigger series focus const DataGrid = (props: { bus: typeof bus }) => { const handleRowHover = (seriesLabel: string) => { props.bus.setData("focusSeries", { sourceId: "data-grid", targets: [{ label: seriesLabel }], }); }; return <table>{/* Grid implementation */}</table>; }; // Chart and grid interact through shared bus const MyDashboard = () => { return ( <div> <DataGrid bus={bus} /> <SolidUplot data={data} pluginBus={bus} plugins={[focusSeries()]} /> </div> ); };
type SolidUplotEvents = { /** Callback fired when the uPlot instance is created */ readonly onCreate?: (u: uPlot, meta: OnCreateMeta) => void; /** Callback fired when the cursor moves */ readonly onCursorMove?: (params: OnCursorMoveParams) => void; }; // Main component props (extends all uPlot.Options except plugins, width, height) type SolidUplotProps<T extends VoidStruct = VoidStruct> = SolidUplotOptions<T> & SolidUplotEvents & { // Ref callback to access the chart container element ref?: Ref<HTMLDivElement>; // CSS class name for the chart container (default: "solid-uplot") // Additional classes will be appended to the default class class?: string; // CSS styles for the chart container (position is managed internally) style?: Omit<JSX.CSSProperties, "position">; // Enable automatic resizing to fit container (default: false) autoResize?: boolean; // Whether to reset scales when chart data is updated (default: true) resetScales?: boolean; // Where to place children components relative to the chart (default: "top") childrenPlacement?: "top" | "bottom"; }; // Configuration options extending uPlot.Options with SolidJS enhancements type SolidUplotOptions<T extends VoidStruct = VoidStruct> = Omit< uPlot.Options, "plugins" | "width" | "height" > & { // Chart dimensions width?: number; // default: 600 height?: number; // default: 300 // Plugin configuration plugins?: SolidUplotPlugin<T>[]; pluginBus?: SolidUplotPluginBus<T>; }; // Plugin type (can be standard uPlot plugin or factory function) type SolidUplotPlugin<T extends VoidStruct = VoidStruct> = uPlot.Plugin | UplotPluginFactory<T>;
// Plugin bus type (derived from createPluginBus return type) type SolidUplotPluginBus<T extends VoidStruct = VoidStruct> = ReturnType<typeof createPluginBus<T>>; // Create a plugin bus const createPluginBus: <T extends VoidStruct = VoidStruct>( initialData?: T, ) => SolidUplotPluginBus<T>;
// Cursor Plugin const cursor = (): UplotPluginFactory<CursorPluginMessageBus>; // Focus Series Plugin const focusSeries = (options?: { pxThreshold?: number; // default: 15 }): UplotPluginFactory<CursorPluginMessageBus & FocusSeriesPluginMessageBus>; // Tooltip Plugin const tooltip = ( Component: Component<TooltipProps>, options?: { placement?: "top-left" | "top-right" | "bottom-left" | "bottom-right"; id?: string; class?: string; style?: JSX.CSSProperties; zIndex?: number; // default: 20 } ): UplotPluginFactory<CursorPluginMessageBus & FocusSeriesPluginMessageBus>; // Legend Plugin const legend = ( Component: Component<LegendProps>, options?: { placement?: "top-left" | "top-right"; // default: "top-left" pxOffset?: number; // default: 8 id?: string; class?: string; style?: JSX.CSSProperties; zIndex?: number; // default: 10 } ): UplotPluginFactory<CursorPluginMessageBus & FocusSeriesPluginMessageBus>;
import { SolidUplot } from "@dschz/solid-uplot"; const BasicChart = () => { return ( <SolidUplot data={[ [0, 1, 2, 3], [10, 20, 30, 40], [15, 25, 35, 45], ]} width={600} height={400} scales={{ x: { time: false }, }} series={[ {}, { label: "Series 1", stroke: "#1f77b4" }, { label: "Series 2", stroke: "#ff7f0e" }, ]} /> ); };
import { SolidUplot, createPluginBus } from "@dschz/solid-uplot"; import { cursor, tooltip, legend, focusSeries } from "@dschz/solid-uplot/plugins"; import type { CursorPluginMessageBus, FocusSeriesPluginMessageBus, TooltipProps, LegendProps, } from "@dschz/solid-uplot/plugins"; const MyTooltip: Component<TooltipProps> = (props) => ( <div style={{ background: "white", padding: "8px", border: "1px solid #ccc" }}> <div>Time: {new Date(props.cursor.xValue * 1000).toLocaleTimeString()}</div> <For each={props.seriesData}> {(series) => { const value = props.u.data[series.seriesIdx]?.[props.cursor.idx]; return ( <div style={{ color: series.stroke }}> {series.label}: {value?.toFixed(2)} </div> ); }} </For> </div> ); const MyLegend: Component<LegendProps> = (props) => { const cursorVisible = () => props.bus.data.cursor?.state[props.u.root.id]?.visible; return ( <div style={{ background: "white", border: "1px solid #ddd", padding: "8px", opacity: cursorVisible() ? 0.6 : 1, transition: "opacity 200ms", }} > <For each={props.seriesData}> {(series) => ( <div style={{ display: "flex", "align-items": "center", gap: "6px" }}> <div style={{ width: "12px", height: "12px", background: series.stroke, }} /> <span>{series.label}</span> </div> )} </For> </div> ); }; const FullFeaturedChart = () => { const bus = createPluginBus<CursorPluginMessageBus & FocusSeriesPluginMessageBus>(); return ( <SolidUplot data={[ [0, 1, 2, 3, 4, 5], [10, 20, 30, 40, 50, 60], [15, 25, 35, 45, 55, 65], [5, 15, 25, 35, 45, 55], ]} width={800} height={500} series={[ {}, { label: "Revenue", stroke: "#1f77b4" }, { label: "Profit", stroke: "#ff7f0e" }, { label: "Expenses", stroke: "#2ca02c" }, ]} plugins={[ cursor(), focusSeries({ pxThreshold: 20 }), tooltip(MyTooltip, { placement: "top-right" }), legend(MyLegend, { placement: "top-left", pxOffset: 12 }), ]} pluginBus={bus} /> ); };
const ResponsiveChart = () => { return ( <div style={{ width: "100%", height: "400px", border: "1px solid #ccc" }}> <SolidUplot autoResize={true} data={data} series={series} plugins={[cursor(), tooltip(MyTooltip)]} /> </div> ); };
const Dashboard = () => { const bus = createPluginBus<FocusSeriesPluginMessageBus>(); const handleSeriesToggle = (seriesLabel: string) => { bus.setData("focusSeries", { sourceId: "external-control", targets: [{ label: seriesLabel }], }); }; return ( <div> <div> <button onClick={() => handleSeriesToggle("Series 1")}>Focus Series 1</button> <button onClick={() => handleSeriesToggle("Series 2")}>Focus Series 2</button> </div> <SolidUplot data={data} plugins={[focusSeries()]} pluginBus={bus} /> </div> ); };
Contributions are welcome! Please feel free to submit a Pull Request.
Also check out the Discord community.
MIT