diff --git a/chartlets.js/src/lib/components/Plot.tsx b/chartlets.js/src/lib/components/Plot.tsx index ef21832c..62d939b7 100644 --- a/chartlets.js/src/lib/components/Plot.tsx +++ b/chartlets.js/src/lib/components/Plot.tsx @@ -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 { onChange: ComponentChangeHandler; } export function Plot({ id, style, chart, onChange }: PlotProps) { - const signalListeners = useSignalListeners(chart, id, onChange); - if (!chart) { return
; } - + const { datasets, ...spec } = chart; + const handleSignal = (_signalName: string, value: unknown) => { + if (id) { + return onChange({ + componentType: "Plot", + id: id, + property: "points", + value: value, + }); + } + }; return ( ); diff --git a/chartlets.js/src/lib/components/Select.tsx b/chartlets.js/src/lib/components/Select.tsx index d238b64c..24610abd 100644 --- a/chartlets.js/src/lib/components/Select.tsx +++ b/chartlets.js/src/lib/components/Select.tsx @@ -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 { onChange: ComponentChangeHandler; @@ -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()]; diff --git a/chartlets.js/src/lib/hooks.ts b/chartlets.js/src/lib/hooks.ts index dfbf932e..f81e6c51 100644 --- a/chartlets.js/src/lib/hooks.ts +++ b/chartlets.js/src/lib/hooks.ts @@ -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, @@ -44,104 +38,6 @@ export function makeContributionsHook( }; } -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`. diff --git a/chartlets.js/src/lib/types/state/component.ts b/chartlets.js/src/lib/types/state/component.ts index 9bc53829..8b6fd8e4 100644 --- a/chartlets.js/src/lib/types/state/component.ts +++ b/chartlets.js/src/lib/types/state/component.ts @@ -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" @@ -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; // Add the datasets property + }) | null; } @@ -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 { diff --git a/chartlets.js/src/lib/types/state/vega.ts b/chartlets.js/src/lib/types/state/vega.ts deleted file mode 100644 index 73f0cace..00000000 --- a/chartlets.js/src/lib/types/state/vega.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type { TopLevelParameter } from "vega-lite/src/spec/toplevel"; -import type { TopLevelSelectionParameter } from "vega-lite/src/selection"; - -export type SignalHandler = (signalName: string, value: unknown) => void; - -/* - There are two types of Parameters in Vega-lite. Variable and Selection parameters. We need to check if the provided - parameter in the chart from the server is a Selection parameter so that we can extract the selection event types - (point or interval) and further, the events from the `select.on` property (e.g. click, mouseover, keydown etc.) if - that property `on` exists. We need these events and their names to create the signal listeners and pass them to the - Vega-lite element for event-handling (signal-listeners). - */ -export function isTopLevelSelectionParameter( - param: TopLevelParameter, -): param is TopLevelSelectionParameter { - return "select" in param; -} diff --git a/chartlets.js/src/lib/utils/isString.ts b/chartlets.js/src/lib/utils/isString.ts deleted file mode 100644 index 9be9b393..00000000 --- a/chartlets.js/src/lib/utils/isString.ts +++ /dev/null @@ -1,3 +0,0 @@ -export function isString(signalName: unknown): signalName is string { - return typeof signalName === "string"; -} diff --git a/chartlets.js/src/lib/utils/objPath.ts b/chartlets.js/src/lib/utils/objPath.ts index 45156f9a..c335f8a4 100644 --- a/chartlets.js/src/lib/utils/objPath.ts +++ b/chartlets.js/src/lib/utils/objPath.ts @@ -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; @@ -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 diff --git a/chartlets.py/my_extension/my_panel_1.py b/chartlets.py/my_extension/my_panel_1.py index 9a6af188..d50822e5 100644 --- a/chartlets.py/my_extension/my_panel_1.py +++ b/chartlets.py/my_extension/my_panel_1.py @@ -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 @@ -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. @@ -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 diff --git a/chartlets.py/my_extension/my_panel_2.py b/chartlets.py/my_extension/my_panel_2.py index 6bee4c93..9188ab6c 100644 --- a/chartlets.py/my_extension/my_panel_2.py +++ b/chartlets.py/my_extension/my_panel_2.py @@ -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