Skip to content

Commit

Permalink
feat: adds uplot-solid implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
thedanchez committed Sep 30, 2024
1 parent 7b7705c commit b0dd467
Show file tree
Hide file tree
Showing 8 changed files with 336 additions and 70 deletions.
73 changes: 65 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,71 @@
# Template: SolidJS Library
# μPlot Solid

Template for [SolidJS](https://www.solidjs.com/) library package. Bundling of the library is managed by [tsup](https://tsup.egoist.dev/).
Solid wrapper around [μPlot](https://github.com/leeoniya/uPlot?tab=readme-ov-file#-%CE%BCplot) which is a small, fast and performant 2D canvas based chart for time series, lines, areas, ohlc & bars. It exposes the μPlot API in a fully typed, declarative JSX format and does the work to make the μPlot experience as reactive as possible.

Other things configured include:
Once a μPlot instance is made, the component will **not** recreate a new instance even if the `data` or `size` of the chart updates reactively. However, it will create a new chart instance if any other options are reactively updated when using it (e.g. `series`, `hooks`, `plugins`, etc.).

- Bun (for dependency management and running scripts)
- TypeScript
- ESLint / Prettier
- Solid Testing Library + Vitest (for testing)
- GitHub Actions (for all CI/CD)
## Installing

```bash
npm install uplot uplot-solid
pnpm add uplot uplot-solid
yarn add uplot uplot-solid
bun add uplot uplot-solid
```

## Overview

If you are new to μPlot, here is a quick breakdown of some things using a simple example plot:

```tsx
<Uplot
width={1000}
height={300}
data={[
[], // x-series data
[], // y-series-1 data
[], // y-series-2 data
// etc...
]}
series={[
{
label: "Time",
stroke: "green",
},
{
label: "Y Series 1",
stroke: "blue",
},
{
label: "Y Series 2",
stroke: "red",
},
// etc...
]}
/>
```

The above is the minimum of what you need to get a chart on screen given you have filled in the data. The expected format of `data` is a 2D array where the first array is the dataset for the x-series of the plot. The `series` list follows the same ordering being a list of config objects where the first object applies to the x-series and the rest to the y-series. μPlot supports a single x-series and all the y-series share the same x-series data. There are many more fields within the series struct that you can configure to tailor the display of the series data to your liking.

Here are some other key options within the API to familiarize yourself with:

- `hooks`: An object structure exposing key events that occur within μPlot. Every event hook accepts an **array of callbacks** allowing you to have discrete units of work that occur within a single event.
- `plugins`: A **plugin** is used to extend or modify the behavior of the chart by injecting custom functionality. You can hook into the various lifecycle events (the very same ones exposed from the `hooks` property) and add custom behavior, such as drawing on the canvas, adding custom controls, tooltips, interactions, or integrating with external libraries.

### Why Use Plugins Instead of Hooks?

In a nutshell, separation of concerns. Here are some points:

- **Customization**: Add custom drawings (like annotations, lines, or custom tooltips).
- **Modularity**: Keep the core logic of the chart clean and separate from specific custom behavior.
- **Reusability**: Create reusable plugins that can be applied across multiple uPlot instances with consistent functionality.
- **Extensibility**: Augment the chart with additional features (e.g., zoom controls, data overlays).

### Demos

Be sure to check out the [μPlot demos](https://leeoniya.github.io/uPlot/demos/index.html) to see the many kinds of different charts you can create. The code for the demos can be found [here](https://github.com/leeoniya/uPlot/tree/master/demos).

Over time, I will try to add example demos using this library to showcase some simple and more complex examples (e.g. creating custom tooltips either via `plugins` or using Solid directly)

## Getting Started

Expand Down
Binary file modified bun.lockb
Binary file not shown.
44 changes: 38 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,25 @@
{
"name": "template-solidjs-library",
"version": "0.0.0",
"description": "Template for SolidJS library using tsup for bundling. Configured with Bun, NVM, TypeScript, ESLint, Prettier, Vitest, and GHA",
"name": "uplot-solid",
"version": "1.0.0",
"description": "Solid wrapper for μPlot",
"type": "module",
"author": "Daniel Sanchez <[email protected]>",
"license": "MIT",
"homepage": "https://github.com/thedanchez/uplot-solid#readme",
"bugs": {
"url": "https://github.com/thedanchez/uplot-solid/issues"
},
"files": [
"dist",
"README.md"
],
"keywords": [
"uPlot",
"Solid",
"SolidJS",
"charts",
"plot"
],
"scripts": {
"build": "tsup",
"build:watch": "tsup --watch",
Expand All @@ -17,27 +34,42 @@
"test:cov": "vitest run --coverage",
"typecheck": "tsc --noEmit"
},
"license": "MIT",
"devDependencies": {
"@solidjs/testing-library": "^0.8.10",
"@testing-library/jest-dom": "^6.5.0",
"@types/bun": "^1.1.10",
"@typescript-eslint/eslint-plugin": "^8.7.0",
"@typescript-eslint/parser": "^8.7.0",
"@vitest/coverage-istanbul": "^2.1.1",
"esbuild-css-modules-plugin": "^3.1.2",
"eslint": "^8.57.0",
"eslint-plugin-simple-import-sort": "^12.1.1",
"eslint-plugin-solid": "^0.14.3",
"jsdom": "^25.0.1",
"prettier": "^3.3.3",
"solid-js": "^1.9.1",
"tsup": "^8.3.0",
"tsup-preset-solid": "^2.2.0",
"typescript": "^5.6.2",
"uplot": "^1.6.31",
"vite": "^5.4.8",
"vite-plugin-solid": "^2.10.2",
"vitest": "^2.1.1"
},
"peerDependencies": {
"solid-js": ">=1.8.0"
}
"solid-js": ">=1.8.0",
"uplot": ">=1.6.31"
},
"main": "./dist/index.js",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"browser": {},
"exports": {
"solid": "./dist/index.jsx",
"import": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
}
},
"typesVersions": {}
}
140 changes: 89 additions & 51 deletions playground/App.tsx
Original file line number Diff line number Diff line change
@@ -1,61 +1,99 @@
import { createSignal, For } from "solid-js";
import { createStore } from "solid-js/store";
import { createSignal } from "solid-js";

type Todo = { id: number; text: string; completed: boolean };
import UplotSolid from "../dist";
import fakeData from "./resource/uplot_fake_data.json";

const isNil = (value: unknown): value is null | undefined => value === null || value === undefined;

export const App = () => {
let input!: HTMLInputElement;
const [size, setSize] = createSignal({ width: 1000, height: 300 });
const [position, setPosition] = createSignal({ left: 0, top: 0 });
const [content, setContent] = createSignal("");

const [count, setCount] = createSignal(0);
const [todos, setTodos] = createStore<Todo[]>([]);
const alignedData = [fakeData.time, fakeData.y_series_1, fakeData.y_series_2] as uPlot.AlignedData;

const addTodo = (text: string) => {
setTodos(todos.length, { id: todos.length, text, completed: false });
};
return (
<div id="app-playground">
<button
onClick={() => {
setSize({ width: 1000, height: size().height === 300 ? 600 : 300 });
}}
>
toggle height
</button>
<UplotSolid
id="snapshot-plot"
data={alignedData}
width={size().width}
height={size().height}
series={[
{
label: "Time",
stroke: "green",
},
{
label: "Series 2",
stroke: "blue",
},
{
label: "Series 3",
stroke: "red",
},
]}
hooks={{
setCursor: [
(u) => {
const { idx } = u.cursor;

const toggleTodo = (id: number) => {
setTodos(id, "completed", (c) => !c);
};
const left = u.cursor.left as number;
const top = u.cursor.top as number;

return (
<>
<div>
<div>Count: {count()}</div>
<button
onClick={() => {
setCount((prev) => prev + 1);
}}
>
Increment Count
</button>
<input placeholder="new todo here" ref={input} />
<button
onClick={() => {
if (!input.value.trim()) return;
addTodo(input.value);
input.value = "";
}}
>
Add Todo
</button>
</div>
<For each={todos}>
{(todo) => {
const { id, text } = todo;
return (
<div>
<input type="checkbox" checked={todo.completed} onChange={[toggleTodo, id]} />
<span
style={{
"text-decoration": todo.completed ? "line-through" : "none",
}}
>
{text}
</span>
</div>
);
if (!isNil(idx)) {
const xValue = u.data[0][idx] as number;
const yValue = u.data[1]?.[idx] as number;
const yValue2 = u.data[2]?.[idx] as number;

setPosition({ left: left - 90, top: top + 40 });
// setContent(`x: ${xValue}, y: ${yValue}`);
setContent(
`x: ${new Date(xValue).toLocaleTimeString()}, y1: ${yValue}, y2: ${yValue2}. Pluse randome number: ${Math.random()}`,
);
} else {
setContent(""); // Hide tooltip
}
},
],
}}
</For>
</>
/>
<Tooltip position={position()} content={content()} />
</div>
);
};

type TooltipProps = {
readonly position: { left: number; top: number };
readonly content: string;
};

function Tooltip(props: TooltipProps) {
return (
<div
id="tooltip"
style={{
position: "absolute",
background: "black",
color: "white",
padding: "5px",
border: "1px solid black",
"border-radius": "3px",
"pointer-events": "none",
display: props.content ? "block" : "none",
left: `${props.position.left}px`,
top: `${props.position.top}px`,
"z-index": 100,
}}
>
{props.content}
</div>
);
}
70 changes: 70 additions & 0 deletions playground/resource/uplot_fake_data.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
{
"time": [
1727395200000, 1727395500000, 1727395800000, 1727396100000, 1727396400000, 1727396700000, 1727397000000,
1727397300000, 1727397600000, 1727397900000, 1727398200000, 1727398500000, 1727398800000, 1727399100000,
1727399400000, 1727399700000, 1727400000000, 1727400300000, 1727400600000, 1727400900000, 1727401200000,
1727401500000, 1727401800000, 1727402100000, 1727402400000, 1727402700000, 1727403000000, 1727403300000,
1727403600000, 1727403900000, 1727404200000, 1727404500000, 1727404800000, 1727405100000, 1727405400000,
1727405700000, 1727406000000, 1727406300000, 1727406600000, 1727406900000, 1727407200000, 1727407500000,
1727407800000, 1727408100000, 1727408400000, 1727408700000, 1727409000000, 1727409300000, 1727409600000,
1727409900000, 1727410200000, 1727410500000, 1727410800000, 1727411100000, 1727411400000, 1727411700000,
1727412000000, 1727412300000, 1727412600000, 1727412900000, 1727413200000, 1727413500000, 1727413800000,
1727414100000, 1727414400000, 1727414700000, 1727415000000, 1727415300000, 1727415600000, 1727415900000,
1727416200000, 1727416500000, 1727416800000, 1727417100000, 1727417400000, 1727417700000, 1727418000000,
1727418300000, 1727418600000, 1727418900000, 1727419200000, 1727419500000, 1727419800000, 1727420100000,
1727420400000, 1727420700000, 1727421000000, 1727421300000, 1727421600000, 1727421900000, 1727422200000,
1727422500000, 1727422800000, 1727423100000, 1727423400000, 1727423700000, 1727424000000, 1727424300000,
1727424600000, 1727424900000, 1727425200000, 1727425500000, 1727425800000, 1727426100000, 1727426400000,
1727426700000, 1727427000000, 1727427300000, 1727427600000, 1727427900000, 1727428200000, 1727428500000,
1727428800000, 1727429100000, 1727429400000, 1727429700000, 1727430000000, 1727430300000, 1727430600000,
1727430900000, 1727431200000, 1727431500000, 1727431800000, 1727432100000, 1727432400000, 1727432700000,
1727433000000, 1727433300000, 1727433600000, 1727433900000, 1727434200000, 1727434500000, 1727434800000,
1727435100000, 1727435400000, 1727435700000, 1727436000000, 1727436300000, 1727436600000, 1727436900000,
1727437200000, 1727437500000, 1727437800000, 1727438100000, 1727438400000, 1727438700000, 1727439000000,
1727439300000, 1727439600000, 1727439900000, 1727440200000, 1727440500000, 1727440800000, 1727441100000,
1727441400000, 1727441700000, 1727442000000, 1727442300000, 1727442600000, 1727442900000, 1727443200000,
1727443500000, 1727443800000, 1727444100000, 1727444400000, 1727444700000, 1727445000000, 1727445300000,
1727445600000, 1727445900000, 1727446200000, 1727446500000, 1727446800000, 1727447100000, 1727447400000,
1727447700000, 1727448000000, 1727448300000, 1727448600000, 1727448900000, 1727449200000, 1727449500000,
1727449800000, 1727450100000, 1727450400000, 1727450700000, 1727451000000, 1727451300000, 1727451600000,
1727451900000, 1727452200000, 1727452500000, 1727452800000, 1727453100000, 1727453400000, 1727453700000,
1727454000000, 1727454300000, 1727454600000, 1727454900000, 1727455200000, 1727455500000, 1727455800000,
1727456100000, 1727456400000, 1727456700000, 1727457000000, 1727457300000, 1727457600000, 1727457900000,
1727458200000, 1727458500000, 1727458800000, 1727459100000, 1727459400000, 1727459700000, 1727460000000,
1727460300000, 1727460600000, 1727460900000, 1727461200000, 1727461500000, 1727461800000, 1727462100000,
1727462400000, 1727462700000, 1727463000000, 1727463300000, 1727463600000, 1727463900000, 1727464200000,
1727464500000, 1727464800000, 1727465100000, 1727465400000, 1727465700000, 1727466000000, 1727466300000,
1727466600000, 1727466900000, 1727467200000, 1727467500000, 1727467800000, 1727468100000, 1727468400000,
1727468700000, 1727469000000, 1727469300000, 1727469600000, 1727469900000, 1727470200000, 1727470500000,
1727470800000, 1727471100000, 1727471400000, 1727471700000, 1727472000000, 1727472300000, 1727472600000,
1727472900000, 1727473200000, 1727473500000, 1727473800000, 1727474100000, 1727474400000, 1727474700000,
1727475000000, 1727475300000, 1727475600000, 1727475900000, 1727476200000, 1727476500000, 1727476800000,
1727477100000, 1727477400000, 1727477700000, 1727478000000, 1727478300000, 1727478600000, 1727478900000,
1727479200000, 1727479500000, 1727479800000, 1727480100000, 1727480400000, 1727480700000, 1727481000000,
1727481300000
],
"y_series_1": [
73, 75, 74, 94, 90, 78, 64, 94, 50, 74, 56, 58, 73, 50, 93, 57, 73, 60, 66, 57, 84, 84, 82, 54, 91, 88, 90, 77, 56,
58, 57, 61, 83, 82, 97, 72, 73, 86, 84, 93, 89, 71, 76, 84, 50, 84, 86, 96, 63, 52, 50, 54, 75, 63, 88, 76, 58, 64,
64, 75, 91, 62, 81, 88, 98, 81, 53, 79, 86, 72, 88, 94, 64, 92, 78, 85, 62, 81, 56, 71, 77, 51, 91, 94, 55, 77, 77,
93, 93, 69, 79, 60, 77, 74, 88, 82, 50, 76, 62, 90, 52, 88, 55, 57, 76, 58, 86, 82, 91, 93, 73, 64, 81, 81, 73, 90,
98, 98, 61, 88, 51, 52, 98, 86, 98, 66, 98, 51, 51, 77, 72, 86, 81, 82, 50, 68, 51, 93, 75, 81, 55, 81, 53, 60, 66,
87, 73, 54, 83, 55, 71, 60, 97, 65, 82, 58, 55, 65, 78, 52, 69, 85, 68, 75, 52, 68, 69, 81, 56, 90, 82, 89, 88, 67,
89, 50, 60, 77, 74, 99, 72, 80, 79, 91, 84, 56, 65, 75, 97, 98, 51, 50, 97, 61, 54, 86, 81, 58, 90, 84, 68, 97, 65,
52, 69, 73, 82, 73, 60, 98, 57, 85, 87, 89, 69, 84, 97, 74, 84, 74, 78, 67, 95, 67, 51, 84, 65, 90, 85, 82, 53, 82,
63, 70, 97, 69, 57, 56, 52, 66, 82, 97, 61, 71, 71, 95, 79, 87, 87, 94, 57, 76, 76, 83, 70, 79, 82, 77, 96, 82, 54,
97, 68, 53, 84, 98, 66, 93, 77, 79, 78, 95, 55, 84, 90, 86, 73, 78, 98, 95, 80, 84, 82, 70, 81, 72, 82, 52
],
"y_series_2": [
37, 44, 61, 50, 73, 77, 22, 59, 65, 43, 69, 51, 66, 41, 42, 21, 46, 61, 21, 45, 36, 59, 52, 28, 62, 73, 67, 58, 48,
61, 74, 45, 54, 69, 44, 43, 32, 79, 77, 26, 76, 55, 64, 39, 20, 27, 65, 35, 33, 31, 70, 42, 34, 47, 53, 21, 51, 42,
41, 70, 44, 77, 41, 77, 77, 41, 68, 71, 61, 25, 34, 73, 62, 79, 56, 52, 27, 72, 79, 63, 63, 24, 58, 23, 25, 64, 51,
71, 49, 66, 54, 74, 59, 71, 35, 32, 69, 79, 61, 49, 38, 36, 75, 38, 47, 77, 74, 45, 56, 45, 72, 42, 28, 31, 72, 20,
77, 77, 20, 66, 53, 51, 73, 67, 44, 59, 64, 72, 20, 35, 58, 24, 41, 48, 74, 22, 31, 45, 35, 70, 56, 41, 76, 48, 33,
47, 24, 66, 68, 49, 65, 71, 24, 31, 35, 45, 45, 67, 40, 58, 55, 52, 49, 56, 42, 77, 29, 73, 24, 55, 53, 71, 50, 29,
77, 38, 77, 51, 20, 75, 24, 64, 23, 35, 43, 35, 74, 21, 68, 47, 51, 46, 39, 43, 31, 69, 54, 79, 52, 52, 70, 62, 56,
31, 22, 20, 52, 59, 29, 62, 63, 48, 32, 31, 50, 65, 21, 70, 69, 54, 42, 36, 45, 27, 48, 45, 29, 45, 53, 70, 60, 26,
23, 77, 72, 69, 64, 30, 48, 75, 55, 44, 40, 76, 55, 29, 56, 28, 43, 54, 68, 54, 67, 55, 37, 68, 58, 51, 43, 42, 51,
56, 31, 68, 74, 32, 42, 44, 54, 60, 49, 36, 68, 39, 67, 44, 41, 32, 78, 38, 68, 55, 31, 60, 38, 31, 28, 26
]
}
Loading

0 comments on commit b0dd467

Please sign in to comment.