Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 14 additions & 6 deletions chartlets.js/src/lib/components/Plot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,32 @@ import { VegaLite } from "react-vega";

import { type PlotState } from "@/lib/types/state/component";
import { type ComponentChangeHandler } from "@/lib/types/state/event";
import { useSignalListeners } from "@/lib/hooks";

export interface PlotProps extends Omit<PlotState, "type"> {
onChange: ComponentChangeHandler;
}

export function Plot({ id, style, chart, onChange }: PlotProps) {
const signalListeners = useSignalListeners(chart, id, onChange);

if (!chart) {
return <div id={id} style={style} />;
}

const { datasets, ...spec } = chart;
const handleSignal = (_signalName: string, value: unknown) => {
if (id) {
return onChange({
componentType: "Plot",
id: id,
property: "points",
value: value,
});
}
};
return (
<VegaLite
spec={chart}
spec={spec}
data={datasets}
style={style}
signalListeners={signalListeners}
signalListeners={{ onClick: handleSignal }}
actions={false}
/>
);
Expand Down
3 changes: 1 addition & 2 deletions chartlets.js/src/lib/components/Select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import {
type SelectState,
} from "@/lib/types/state/component";
import { type ComponentChangeHandler } from "@/lib/types/state/event";
import { isString } from "@/lib/utils/isString";

export interface SelectProps extends Omit<SelectState, "type"> {
onChange: ComponentChangeHandler;
Expand Down Expand Up @@ -65,7 +64,7 @@ export function Select({
function normalizeSelectOption(
option: SelectOption,
): [string | number, string] {
if (isString(option)) {
if (typeof option === "string") {
return [option, option];
} else if (typeof option === "number") {
return [option, option.toString()];
Expand Down
106 changes: 1 addition & 105 deletions chartlets.js/src/lib/hooks.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,7 @@
import type { StoreState } from "@/lib/types/state/store";
import { store } from "@/lib/store";
import { useCallback, useMemo } from "react";
import { useMemo } from "react";
import type { ContributionState } from "@/lib/types/state/contribution";
import {
isTopLevelSelectionParameter,
type SignalHandler,
} from "@/lib/types/state/vega";
import type { TopLevelSpec } from "vega-lite/src/spec";
import { isString } from "@/lib/utils/isString";
import type {
ComponentChangeEvent,
ComponentChangeHandler,
Expand Down Expand Up @@ -44,104 +38,6 @@ export function makeContributionsHook<S extends object = object>(
};
}

export function useSignalListeners(
chart: TopLevelSpec | null,
id: string | undefined,
onChange: ComponentChangeHandler,
): { [key: string]: SignalHandler } {
/*
Here, we loop through all the params to create map of signals which will
be then used to create the map of signal-listeners because not all
params are event-listeners, and we need to identify them. Later in the
code, we then see which handlers do we have so that we can create
those listeners with the `name` specified in the event-listener object.
*/
const signals: { [key: string]: string } = useMemo(() => {
if (!chart) return {};
const tempSignals: { [key: string]: string } = {};
chart.params?.forEach((param) => {
if (isTopLevelSelectionParameter(param)) {
if (
typeof param.select === "object" &&
"on" in param.select &&
param.select.on != null
) {
const signalName = param.select.on;
/*
The signal name extracted from the `select.on` property can be
either a string or of Stream type (internal Vega
type). But since we create the selection events in
Altair (in the server) using `on="click"` etc., the event type
will be a string, but we need this type-guard to be sure.
If it is a Stream object, that case is not handled yet.
*/
if (isString(signalName)) {
tempSignals[signalName] = param.name;
} else {
console.warn(
`The signal ${param} is of Stream type` +
" (internal Vega-lite type) which is not handled yet.",
);
}
}
}
});
return tempSignals;
}, [chart]);

const handleClickSignal = useCallback(
(signalName: string, signalValue: unknown) => {
if (id) {
return onChange({
componentType: "Plot",
id: id,
property: signalName,
value: signalValue,
});
}
},
[id, onChange],
);

/*
Currently, we only have click events support, but if more are required,
they can be implemented and added in the map below.
*/
const signalHandlerMap: { [key: string]: SignalHandler } = useMemo(
() => ({
click: handleClickSignal,
}),
[handleClickSignal],
);

/*
This function creates the map of signal listeners based on the `signals`
map computed above.
*/
const createSignalListeners = useCallback(
(signals: { [key: string]: string }) => {
const signalListeners: { [key: string]: SignalHandler } = {};
Object.entries(signals).forEach(([event, signalName]) => {
if (signalHandlerMap[event]) {
signalListeners[signalName] = signalHandlerMap[event];
} else {
console.warn(
"The signal " + event + " is not yet supported in chartlets.js",
);
}
});

return signalListeners;
},
[signalHandlerMap],
);

return useMemo(
() => createSignalListeners(signals),
[createSignalListeners, signals],
);
}

/**
* A hook that retrieves the contributions for the given contribution
* point given by `contribPoint`.
Expand Down
9 changes: 5 additions & 4 deletions chartlets.js/src/lib/types/state/component.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { type CSSProperties } from "react";
import type { VisualizationSpec } from "react-vega";
import { isObject } from "@/lib/utils/isObject";
import type { TopLevelSpec } from "vega-lite/src/spec";
import { isString } from "@/lib/utils/isString";

export type ComponentType =
| "Box"
Expand Down Expand Up @@ -55,7 +54,9 @@ export interface CheckboxState extends ComponentState {
export interface PlotState extends ComponentState {
type: "Plot";
chart:
| TopLevelSpec // This is the vega-lite specification type
| (VisualizationSpec & {
datasets?: Record<string, unknown>; // Add the datasets property
})
| null;
}

Expand All @@ -68,7 +69,7 @@ export interface TypographyState extends ContainerState {
}

export function isComponentState(object: unknown): object is ComponentState {
return isObject(object) && isString(object.type);
return isObject(object) && typeof object.type === "string";
}

export function isContainerState(object: unknown): object is ContainerState {
Expand Down
17 changes: 0 additions & 17 deletions chartlets.js/src/lib/types/state/vega.ts

This file was deleted.

3 changes: 0 additions & 3 deletions chartlets.js/src/lib/utils/isString.ts

This file was deleted.

3 changes: 1 addition & 2 deletions chartlets.js/src/lib/utils/objPath.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { isObject } from "@/lib/utils/isObject";
import { isString } from "@/lib/utils/isString";

export type ObjPath = (string | number)[];
export type ObjPathLike = ObjPath | string | number | undefined | null;
Expand Down Expand Up @@ -84,7 +83,7 @@ export function normalizeObjPath(pathLike: ObjPathLike): ObjPath {
}

export function formatObjPath(objPath: ObjPathLike): string {
if (isString(objPath)) {
if (typeof objPath === "string") {
return objPath;
} else if (Array.isArray(objPath)) {
return objPath
Expand Down
50 changes: 3 additions & 47 deletions chartlets.py/my_extension/my_panel_1.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
import copy
from types import NoneType

import altair as alt
import pandas as pd

from chartlets import Component, Input, Output, State
from chartlets import Component, Input, Output
from chartlets.components import Plot, Box, Select
from chartlets.demo.contribs import Panel
from chartlets.demo.context import Context
Expand Down Expand Up @@ -64,7 +60,7 @@ def make_figure(ctx: Context, selected_dataset: int = 0) -> alt.Chart:
# Create another parameter to handle the click events and send the data as
# specified in the fields
click_param = alt.selection_point(
on="click", name="points", fields=["x", variable_name]
on="click", name="onClick", fields=["x", variable_name]
)
# Create a chart type using mark_* where * could be any kind of chart
# supported by Vega. We can add properties and parameters as shown below.
Expand All @@ -83,45 +79,5 @@ def make_figure(ctx: Context, selected_dataset: int = 0) -> alt.Chart:
.properties(width=290, height=300, title="Vega charts")
.add_params(corner_var, click_param)
)
return chart


@panel.callback(
Input("plot", property="points"), State("plot", "chart"), Output("plot", "chart")
)
def get_click_event_points(ctx: Context, points, plot) -> alt.Chart:
"""
This callback function shows how we can use the event handlers output
(property="points") which was defined in the `make_figure` callback
function as a `on='click'` handler. Here, we access the variables as
defined in the `fields` argument when creating the `click_param` parameter.

Based on the click event, the user can access the point that was clicked.
The function below extracts the points and changes the color of the bar
that was clicked.

"""
if points:
conditions = []
for field, values in points.items():
if field != "vlPoint":
for value in values:
field_type = plot["encoding"].get(field, {}).get("type", "")
if field_type == "nominal":
conditions.append(f"datum.{field} === '{value}'")
else:
conditions.append(f"datum.{field} === {value}")
conditions.append(f"datum.{field} === {repr(value)}")

condition_expr = " && ".join(conditions)

plot["encoding"]["color"] = {
"condition": {
"test": condition_expr,
# Highlight color when the condition is true
"value": "orange",
},
"value": "steelblue", # Default color
}

return alt.Chart.from_dict(plot)
return chart
8 changes: 1 addition & 7 deletions chartlets.py/my_extension/my_panel_2.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,13 +83,7 @@ def make_figure(
)
.properties(width=300, height=300, title="Vega charts using Shorthand syntax")
.add_params(selector)
# .interactive() # Using interactive mode will lead to warning
# `WARN Scale bindings are currently only supported for scales with
# unbinned, continuous domains.`
# because it expects both x and y to be continuous scales,
# but we have x as Nominal which leads to this warning.
# This still works where we can only zoom in on the y axis but
# with a warning.
.interactive()
)
return chart

Expand Down