Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

SolidJS wrapper for uPlot — an ultra-fast, tiny time-series & charting library with a SolidJS enhanced plugin system

License

Notifications You must be signed in to change notification settings

dsnchz/solid-uplot

Repository files navigation

@dschz/solid-uplot banner

@dschz/solid-uplot

License uPlot npm Bundle Size JSR CI Discord

💹 SolidJS wrapper for uPlot — an ultra-fast, small footprint charting library for time-series data.

✨ Features

  • ✅ 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

📦 Installation

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

🎮 Live Demo

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.

Running the Playground

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

📁 Package Structure

This package provides three main export paths for different functionality:

@dschz/solid-uplot

Core components and plugin system:

import { SolidUplot, createPluginBus } from "@dschz/solid-uplot";
import type {
 SolidUplotPluginBus,
 UplotPluginFactory,
 UplotPluginFactoryContext,
} from "@dschz/solid-uplot";

@dschz/solid-uplot/plugins

This export path provides four plugins (three of which can be considered primitives).

  • cursor: transmits cursor position data
  • focusSeries: transmits which series are visually emphasized
  • tooltip: plugin that allows you to present a custom JSX tooltip around the cursor
  • legend: 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";

@dschz/solid-uplot/utils

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";

🚀 Quick Start

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}
 />
 );
};

📏 Responsive Sizing

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>
 );
};

🔌 Enhanced Plugin System

The cornerstone feature of SolidUplot is its refined plugin system that enables extensible functionality and inter-plugin communication through a reactive message bus.

Plugin Bus Architecture

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

Built-in Plugins

Cursor Plugin

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.

Focus Series Plugin

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)
});

Tooltip Plugin

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,
});

Legend Plugin

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

Plugin Bus Type Safety

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)]}
 />
 );
};

Creating Custom Plugins

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);
 },
 },
 };
 };
};

External Component Integration

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>
 );
};

🔧 API Reference

SolidUplot Component

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

// 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>;

Built-in Plugin Options

// 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>;

📚 Examples

Basic Chart

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" },
 ]}
 />
 );
};

Chart with All Plugins

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}
 />
 );
};

Responsive Chart

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>
 );
};

External Integration

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>
 );
};

🤝 Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

Also check out the Discord community.

📄 License

MIT

About

SolidJS wrapper for uPlot — an ultra-fast, tiny time-series & charting library with a SolidJS enhanced plugin system

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages

AltStyle によって変換されたページ (->オリジナル) /