Skip to content

Commit 5e6fb70

Browse files
committed
feat: initial implementation of solid-highcharts
1 parent d25bc09 commit 5e6fb70

File tree

7 files changed

+501
-8
lines changed

7 files changed

+501
-8
lines changed

bun.lock

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

jsr.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"name": "template-solidjs-library",
2+
"name": "@dschz/solid-highcharts",
33
"version": "0.0.0",
44
"license": "MIT",
55
"exports": "./src/index.tsx",

package.json

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
11
{
2-
"name": "template-solidjs-library",
2+
"name": "@dschz/solid-highcharts",
33
"version": "0.0.0",
4-
"description": "Template for SolidJS library using tsup for bundling. Configured with Bun, NVM, TypeScript, ESLint, Prettier, Vitest, and GHA",
4+
"description": "SolidJS wrapper for Highcharts",
55
"type": "module",
66
"author": "Daniel Sanchez <dsanc89@icloud.com>",
77
"license": "MIT",
8-
"homepage": "https://github.com/thedanchez/template-solidjs-library#readme",
8+
"homepage": "https://github.com/dsnchz/solid-highcharts#readme",
99
"repository": {
1010
"type": "git",
11-
"url": "https://github.com/thedanchez/template-solidjs-library.git"
11+
"url": "https://github.com/dsnchz/solid-highcharts.git"
1212
},
1313
"bugs": {
14-
"url": "https://github.com/thedanchez/template-solidjs-library/issues"
14+
"url": "https://github.com/dsnchz/solid-highcharts/issues"
1515
},
1616
"publishConfig": {
1717
"access": "public"
@@ -63,6 +63,7 @@
6363
"eslint-plugin-simple-import-sort": "^12.1.1",
6464
"eslint-plugin-solid": "^0.14.5",
6565
"globals": "^16.1.0",
66+
"highcharts": "^12.2.0",
6667
"jiti": "^2.4.2",
6768
"jsdom": "^26.1.0",
6869
"prettier": "^3.5.3",
@@ -77,6 +78,7 @@
7778
"vitest": "^3.1.3"
7879
},
7980
"peerDependencies": {
81+
"highcharts": ">=12.0.0",
8082
"solid-js": ">=1.6.0"
8183
}
8284
}
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
import { render } from "@solidjs/testing-library";
2+
import type * as Highcharts from "highcharts";
3+
import { beforeEach, describe, expect, test, vi } from "vitest";
4+
5+
import { createChartComponent, type HighchartsConstructor } from "../createChartComponent";
6+
import type { HighchartsModule } from "../types";
7+
8+
// Mock Highcharts
9+
const mockChart = {
10+
update: vi.fn(),
11+
destroy: vi.fn(),
12+
container: {} as HTMLElement,
13+
options: {} as Highcharts.Options,
14+
series: [],
15+
addSeries: vi.fn(),
16+
removeSeries: vi.fn(),
17+
redraw: vi.fn(),
18+
reflow: vi.fn(),
19+
exportChart: vi.fn(),
20+
print: vi.fn(),
21+
getSVG: vi.fn(),
22+
getCSV: vi.fn(),
23+
getTable: vi.fn(),
24+
setTitle: vi.fn(),
25+
setSize: vi.fn(),
26+
showLoading: vi.fn(),
27+
hideLoading: vi.fn(),
28+
} as Partial<Highcharts.Chart> as Highcharts.Chart;
29+
30+
const MockHighchartsModule = {
31+
chart: vi.fn((_container, _options, _onCreateChart) => {
32+
_onCreateChart?.(mockChart);
33+
return mockChart;
34+
}),
35+
stockChart: vi.fn(() => mockChart),
36+
mapChart: vi.fn(() => mockChart),
37+
ganttChart: vi.fn(() => mockChart),
38+
Chart: vi.fn(() => mockChart),
39+
SVGRenderer: vi.fn(),
40+
VMLRenderer: vi.fn(),
41+
color: vi.fn(),
42+
dateFormat: vi.fn(),
43+
numberFormat: vi.fn(),
44+
merge: vi.fn(),
45+
extend: vi.fn(),
46+
each: vi.fn(),
47+
pick: vi.fn(),
48+
wrap: vi.fn(),
49+
addEvent: vi.fn(),
50+
removeEvent: vi.fn(),
51+
fireEvent: vi.fn(),
52+
animate: vi.fn(),
53+
stop: vi.fn(),
54+
getOptions: vi.fn(),
55+
setOptions: vi.fn(),
56+
// Add version to satisfy type requirements
57+
version: "12.0.0",
58+
} as unknown as HighchartsModule;
59+
60+
describe("createChartComponent", () => {
61+
let defaultOptions: Highcharts.Options;
62+
let ref: HTMLDivElement | undefined;
63+
64+
beforeEach(() => {
65+
vi.resetAllMocks();
66+
ref = undefined;
67+
defaultOptions = {
68+
title: { text: "Test Chart" },
69+
series: [{ type: "line", data: [1, 2, 3] }],
70+
};
71+
});
72+
73+
test("throws when passing invalid constructor", () => {
74+
expect(() =>
75+
createChartComponent(MockHighchartsModule, "invalidConstructor" as HighchartsConstructor),
76+
).toThrow();
77+
});
78+
79+
test("can render using chart constructor", () => {
80+
const Chart = createChartComponent(MockHighchartsModule);
81+
82+
const { container } = render(() => <Chart {...defaultOptions} ref={(c) => (ref = c)} />);
83+
84+
expect(container).toBeInTheDocument();
85+
expect(MockHighchartsModule.chart).toHaveBeenCalledWith(ref, defaultOptions, undefined);
86+
});
87+
88+
test("can render using stockChart constructor", () => {
89+
const StockChart = createChartComponent(MockHighchartsModule, "stockChart");
90+
91+
const { container } = render(() => <StockChart {...defaultOptions} ref={(c) => (ref = c)} />);
92+
93+
expect(container).toBeInTheDocument();
94+
expect(MockHighchartsModule.stockChart).toHaveBeenCalledWith(ref, defaultOptions, undefined);
95+
});
96+
97+
test("can render using mapChart constructor", () => {
98+
const MapChart = createChartComponent(MockHighchartsModule, "mapChart");
99+
100+
const { container } = render(() => <MapChart {...defaultOptions} ref={(c) => (ref = c)} />);
101+
102+
expect(container).toBeInTheDocument();
103+
expect(MockHighchartsModule.mapChart).toHaveBeenCalledWith(ref, defaultOptions, undefined);
104+
});
105+
106+
test("can render using ganttChart constructor", () => {
107+
const GanttChart = createChartComponent(MockHighchartsModule, "ganttChart");
108+
109+
const { container } = render(() => <GanttChart {...defaultOptions} ref={(c) => (ref = c)} />);
110+
111+
expect(container).toBeInTheDocument();
112+
expect(MockHighchartsModule.ganttChart).toHaveBeenCalledWith(ref, defaultOptions, undefined);
113+
});
114+
115+
test("destroys the chart when unmounting", () => {
116+
const Chart = createChartComponent(MockHighchartsModule, "chart");
117+
let chartInstance!: Highcharts.Chart;
118+
119+
const { container, unmount } = render(() => (
120+
<Chart
121+
{...defaultOptions}
122+
ref={(c) => (ref = c)}
123+
onCreateChart={(c) => {
124+
chartInstance = c;
125+
}}
126+
/>
127+
));
128+
129+
expect(container).toBeInTheDocument();
130+
expect(MockHighchartsModule.chart).toHaveBeenCalledWith(
131+
ref,
132+
defaultOptions,
133+
expect.any(Function),
134+
);
135+
136+
expect(chartInstance).toBeDefined();
137+
138+
unmount();
139+
140+
expect(chartInstance.destroy).toHaveBeenCalledTimes(1);
141+
});
142+
});

src/createChartComponent.tsx

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import { createEffect, type JSX, mergeProps, onCleanup, onMount, splitProps } from "solid-js";
2+
3+
import type { HighchartsModule } from "./types";
4+
5+
/**
6+
* Supported Highcharts constructor types.
7+
* Each constructor type corresponds to a different chart variant.
8+
*/
9+
export type HighchartsConstructor = "chart" | "stockChart" | "mapChart" | "ganttChart";
10+
11+
/**
12+
* Props for Highcharts chart components.
13+
* Extends Highcharts.Options with additional SolidJS-specific properties.
14+
*
15+
* @example
16+
* ```tsx
17+
* const chartProps: HighchartsComponentProps = {
18+
* title: { text: 'My Chart' },
19+
* series: [{ type: 'line', data: [1, 2, 3] }],
20+
* onCreateChart: (chart) => console.log('Chart created:', chart),
21+
* class: 'my-chart-container'
22+
* };
23+
* ```
24+
*/
25+
export type HighchartsComponentProps = Highcharts.Options & {
26+
/**
27+
* Callback function called when the chart is successfully created.
28+
* Receives the Highcharts chart instance as a parameter.
29+
*/
30+
readonly onCreateChart?: Highcharts.ChartCallbackFunction;
31+
32+
/**
33+
* Ref callback to access the chart container div element.
34+
* Called with the HTMLDivElement that contains the chart.
35+
*/
36+
readonly ref?: (container: HTMLDivElement) => void;
37+
38+
/** CSS class name to apply to the chart container. */
39+
readonly class?: string;
40+
41+
/** CSS styles to apply to the chart container. */
42+
readonly style?: JSX.CSSProperties;
43+
};
44+
45+
/**
46+
* Creates a SolidJS component for rendering Highcharts charts.
47+
*
48+
* This is the core factory function that creates chart components with proper
49+
* SolidJS integration, including reactive updates and cleanup.
50+
*
51+
* @param HighchartsModule - The Highcharts module to use (core, stock, maps, or gantt)
52+
* @param constructor - The chart constructor type to use (defaults to "chart")
53+
* @returns A SolidJS component function that renders Highcharts charts
54+
*
55+
* @throws {Error} When the specified constructor is not found on the Highcharts module
56+
*
57+
* @example
58+
* ```tsx
59+
* import Highcharts from 'highcharts';
60+
* import { createChartComponent } from '@dschz/solid-highcharts';
61+
*
62+
* const Chart = createChartComponent(Highcharts, 'chart');
63+
*
64+
* function App() {
65+
* return (
66+
* <Chart
67+
* title={{ text: 'My Chart' }}
68+
* series={[{ type: 'line', data: [1, 2, 3] }]}
69+
* onCreateChart={(chart) => console.log('Chart ready!', chart)}
70+
* />
71+
* );
72+
* }
73+
* ```
74+
*
75+
* @example
76+
* Using with Highcharts Stock:
77+
* ```tsx
78+
* import Highcharts from 'highcharts/highstock';
79+
*
80+
* const StockChart = createChartComponent(Highcharts, 'stockChart');
81+
*
82+
* function StockApp() {
83+
* return (
84+
* <StockChart
85+
* title={{ text: 'Stock Price' }}
86+
* series={[{ type: 'candlestick', data: stockData }]}
87+
* />
88+
* );
89+
* }
90+
* ```
91+
*/
92+
export const createChartComponent = (
93+
HighchartsModule: HighchartsModule,
94+
constructor: HighchartsConstructor = "chart",
95+
) => {
96+
if (typeof HighchartsModule[constructor] !== "function") {
97+
throw new Error(
98+
`[solid-highcharts] Constructor "${constructor}" not found on Highcharts module. ` +
99+
`Did you import the correct variant (e.g., highstock, highmaps)?`,
100+
);
101+
}
102+
103+
return (props: HighchartsComponentProps) => {
104+
let container!: HTMLDivElement;
105+
let chart!: Highcharts.Chart;
106+
107+
const [local, options] = splitProps(props, ["style", "class", "ref", "onCreateChart"]);
108+
109+
const _local = mergeProps(
110+
{
111+
style: {} as JSX.CSSProperties,
112+
},
113+
local,
114+
);
115+
116+
const _options = mergeProps({}, options);
117+
118+
onMount(() => {
119+
_local.ref?.(container);
120+
chart = HighchartsModule[constructor](container, _options, _local.onCreateChart);
121+
});
122+
123+
createEffect(() => {
124+
chart?.update(options, true, true);
125+
});
126+
127+
onCleanup(() => {
128+
chart?.destroy();
129+
});
130+
131+
return <div ref={container} class={_local.class} style={_local.style} />;
132+
};
133+
};

0 commit comments

Comments
 (0)