Skip to content

Commit 68a4546

Browse files
committed
Isolated Plot into its own sole module that uses Vega; removed MUI dependency
1 parent a0c5292 commit 68a4546

File tree

8 files changed

+161
-145
lines changed

8 files changed

+161
-145
lines changed

chartlets.js/CHANGES.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,15 @@
1+
## Version 0.1.0 (not started)
2+
3+
* Reorganise Chartlets project
4+
- Create `chartlets` GH org.
5+
- Split current `chartlets` repo and move to `chartlets` org:
6+
- `chartlets.py` The Python core package, defines standard components
7+
- `chartlets.py.vega` Defines the `VegaChart` component
8+
- `chartlets.js` The JS library core package
9+
- `chartlets.js.mui` Adds MUI impl. of the standard components
10+
- `chartlets.js.vega` Adds Vega React impl. of the `VegaChart` component
11+
- `chartlets-demo` Chartlets demo which uses all of the above
12+
113
## Version 0.0.30 (in development)
214

315
* The `Plot` component now respects a `theme` property. If not given,

chartlets.js/src/lib/components/Plot.tsx

Lines changed: 0 additions & 47 deletions
This file was deleted.
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { VegaLite } from "react-vega";
2+
import type { TopLevelSpec } from "vega-lite";
3+
4+
import type { ComponentState } from "@/lib/types/state/component";
5+
import type { ComponentProps } from "@/lib/component/Component";
6+
import { useSignalListeners } from "./hooks/useSignalListeners";
7+
import { useTheme, type VegaTheme } from "@/lib/components/Plot/hooks/useTheme";
8+
9+
interface PlotState extends ComponentState {
10+
theme?: VegaTheme | "default" | "system";
11+
chart?:
12+
| TopLevelSpec // This is the vega-lite specification type
13+
| null;
14+
}
15+
16+
interface PlotProps extends ComponentProps, PlotState {}
17+
18+
export function Plot({ type, id, style, theme, chart, onChange }: PlotProps) {
19+
const signalListeners = useSignalListeners(chart, type, id, onChange);
20+
const vegaTheme = useTheme(theme);
21+
if (chart) {
22+
return (
23+
<VegaLite
24+
theme={vegaTheme}
25+
spec={chart}
26+
style={style}
27+
signalListeners={signalListeners}
28+
actions={false}
29+
/>
30+
);
31+
} else {
32+
return <div id={id} style={style} />;
33+
}
34+
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import { useCallback, useMemo } from "react";
2+
import { TopLevelSpec } from "vega-lite";
3+
4+
import { ComponentChangeHandler } from "@/lib/types/state/event";
5+
import { isObject } from "@/lib/utils/isObject";
6+
import { isString } from "@/lib/utils/isString";
7+
8+
type SignalHandler = (signalName: string, signalValue: unknown) => void;
9+
10+
/*
11+
* This is a partial representation of the parameter type from
12+
* SelectionParameter type from the `vega-lite` module. Since we are
13+
* only interested in extracting the handlers, the following
14+
* properties are required.
15+
*/
16+
type SelectionParameter = { name: string; select: { on: string } };
17+
18+
const isSelectionParameter = (param: unknown): param is SelectionParameter =>
19+
isObject(param) &&
20+
"name" in param &&
21+
"select" in param &&
22+
isObject(param.select) &&
23+
param.select?.on !== null &&
24+
isString(param.select.on);
25+
26+
export function useSignalListeners(
27+
chart: TopLevelSpec | null | undefined,
28+
type: string,
29+
id: string | undefined,
30+
onChange: ComponentChangeHandler,
31+
): { [key: string]: SignalHandler } {
32+
/*
33+
* Here, we create map of signals which will be then used to create the
34+
* map of signal-listeners because not all params are event-listeners, and we
35+
* need to identify them. Later in the code, we then see which handlers do we
36+
* have so that we can create those listeners with the `name` specified in
37+
* the event-listener object.
38+
*/
39+
const signals = useMemo(() => {
40+
if (!chart) {
41+
return {};
42+
}
43+
if (!chart.params) {
44+
return {};
45+
}
46+
return chart.params.filter(isSelectionParameter).reduce((acc, param) => {
47+
acc[param.select.on] = param.name;
48+
return acc;
49+
}, {});
50+
}, [chart]);
51+
52+
const handleClickSignal = useCallback(
53+
(signalName: string, signalValue: unknown) => {
54+
if (id) {
55+
return onChange({
56+
componentType: type,
57+
id: id,
58+
property: signalName,
59+
value: signalValue,
60+
});
61+
}
62+
},
63+
[id, onChange, type],
64+
);
65+
66+
/*
67+
* Currently, we only have click events support, but if more are required,
68+
* they can be implemented and added in the map below.
69+
*/
70+
const signalHandlerMap: { [key: string]: SignalHandler } = useMemo(
71+
() => ({
72+
click: handleClickSignal,
73+
}),
74+
[handleClickSignal],
75+
);
76+
77+
/*
78+
* Creates the map of signal listeners based on
79+
* the `signals` map computed above.
80+
*/
81+
return useMemo(() => {
82+
const signalListeners = {};
83+
Object.entries(signals).forEach(([event, signalName]) => {
84+
if (signalHandlerMap[event]) {
85+
signalListeners[signalName] = signalHandlerMap[event];
86+
} else {
87+
console.warn(
88+
`The signal "${event}" is not yet supported in chartlets.js`,
89+
);
90+
}
91+
});
92+
return signalListeners;
93+
}, [signals]);
94+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import * as vegaThemes from "vega-themes";
2+
3+
export type VegaTheme = keyof Omit<typeof vegaThemes, "version">;
4+
5+
const isVegaTheme = (key?: string): key is VegaTheme =>
6+
!!key && key in vegaThemes;
7+
8+
const isSystemThemeDark = () =>
9+
window.matchMedia("(prefers-color-scheme: dark)");
10+
11+
export function useTheme(
12+
theme: VegaTheme | "default" | "system" | string | undefined,
13+
): VegaTheme | undefined {
14+
return theme === "system" && isSystemThemeDark()
15+
? "dark"
16+
: isVegaTheme(theme)
17+
? theme
18+
: undefined;
19+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { Plot } from "./Plot";

chartlets.js/src/lib/hooks.ts

Lines changed: 1 addition & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type { StoreState } from "@/lib/types/state/store";
22
import { store } from "@/lib/store";
33
import { useCallback, useMemo } from "react";
44
import type { ContributionState } from "@/lib/types/state/contribution";
5-
import { type SignalHandler } from "@/lib/types/state/vega";
5+
import { type SignalHandler } from "@/lib/components/Plot/vega";
66
import type { TopLevelSpec } from "vega-lite";
77
import type {
88
ComponentChangeEvent,
@@ -42,102 +42,6 @@ export function makeContributionsHook<S extends object = object>(
4242
};
4343
}
4444

45-
export function useSignalListeners(
46-
chart: TopLevelSpec | null | undefined,
47-
type: string,
48-
id: string | undefined,
49-
onChange: ComponentChangeHandler,
50-
): { [key: string]: SignalHandler } {
51-
/*
52-
This is a partial representation of the parameter type from
53-
SelectionParameter type from the `vega-lite` module. Since we are
54-
only interested in extracting the handlers, the following
55-
properties are required.
56-
*/
57-
type SelectionParameter = { name: string; select: { on: string } };
58-
/*
59-
Here, we create map of signals which will be then used to create the
60-
map of signal-listeners because not all params are event-listeners, and we
61-
need to identify them. Later in the code, we then see which handlers do we
62-
have so that we can create those listeners with the `name` specified in
63-
the event-listener object.
64-
*/
65-
const signals: { [key: string]: string } = useMemo(() => {
66-
if (!chart) return {};
67-
if (!chart.params) return {};
68-
return chart.params
69-
.filter(
70-
(param): param is SelectionParameter =>
71-
isObject(param) &&
72-
param !== null &&
73-
"name" in param &&
74-
"select" in param &&
75-
isObject(param.select) &&
76-
param.select?.on != null &&
77-
isString(param.select.on),
78-
)
79-
.reduce(
80-
(acc, param) => {
81-
acc[param.select.on] = param.name;
82-
return acc;
83-
},
84-
{} as { [key: string]: string },
85-
);
86-
}, [chart]);
87-
88-
const handleClickSignal = useCallback(
89-
(signalName: string, signalValue: unknown) => {
90-
if (id) {
91-
return onChange({
92-
componentType: type,
93-
id: id,
94-
property: signalName,
95-
value: signalValue,
96-
});
97-
}
98-
},
99-
[id, onChange, type],
100-
);
101-
102-
/*
103-
Currently, we only have click events support, but if more are required,
104-
they can be implemented and added in the map below.
105-
*/
106-
const signalHandlerMap: { [key: string]: SignalHandler } = useMemo(
107-
() => ({
108-
click: handleClickSignal,
109-
}),
110-
[handleClickSignal],
111-
);
112-
113-
/*
114-
This function creates the map of signal listeners based on the `signals`
115-
map computed above.
116-
*/
117-
const createSignalListeners = useCallback(
118-
(signals: { [key: string]: string }) => {
119-
const signalListeners: { [key: string]: SignalHandler } = {};
120-
Object.entries(signals).forEach(([event, signalName]) => {
121-
if (signalHandlerMap[event]) {
122-
signalListeners[signalName] = signalHandlerMap[event];
123-
} else {
124-
console.warn(
125-
"The signal " + event + " is not yet supported in chartlets.js",
126-
);
127-
}
128-
});
129-
130-
return signalListeners;
131-
},
132-
[signalHandlerMap],
133-
);
134-
135-
return useMemo(
136-
() => createSignalListeners(signals),
137-
[createSignalListeners, signals],
138-
);
139-
}
140-
14145
/**
14246
* A hook that retrieves the contributions for the given contribution
14347
* point given by `contribPoint`.

chartlets.js/src/lib/types/state/vega.ts

Lines changed: 0 additions & 1 deletion
This file was deleted.

0 commit comments

Comments
 (0)