From 7a18ffbc699f03de87effd3e38ace5b69ee7d4b8 Mon Sep 17 00:00:00 2001 From: Yogesh Kumar Baljeet Singh Date: Wed, 20 Nov 2024 15:18:35 +0100 Subject: [PATCH 01/10] Fix vega OnClick warning --- chartlets.js/src/lib/components/Plot.tsx | 62 ++++++++++++++++++- chartlets.js/src/lib/types/state/component.ts | 6 +- 2 files changed, 62 insertions(+), 6 deletions(-) diff --git a/chartlets.js/src/lib/components/Plot.tsx b/chartlets.js/src/lib/components/Plot.tsx index 62d939b7..1d71a8db 100644 --- a/chartlets.js/src/lib/components/Plot.tsx +++ b/chartlets.js/src/lib/components/Plot.tsx @@ -2,6 +2,9 @@ import { VegaLite } from "react-vega"; import { type PlotState } from "@/lib/types/state/component"; import { type ComponentChangeHandler } from "@/lib/types/state/event"; +import type { TopLevelParameter } from "vega-lite/src/spec/toplevel"; +import type { TopLevelSelectionParameter } from "vega-lite/src/selection"; +import type { Stream } from "vega-typings/types/spec/stream"; export interface PlotProps extends Omit { onChange: ComponentChangeHandler; @@ -11,8 +14,38 @@ export function Plot({ id, style, chart, onChange }: PlotProps) { if (!chart) { return
; } + + function isTopLevelSelectionParameter( + param: TopLevelParameter, + ): param is TopLevelSelectionParameter { + return "select" in param; + } + + function isString(signal_name: string | Stream): signal_name is string { + return typeof signal_name === "string"; + } + + const signals: { [key: string]: string } = {}; + + chart.params?.forEach((param) => { + if (isTopLevelSelectionParameter(param)) { + if ( + typeof param.select === "object" && + "on" in param.select && + param.select.on != null + ) { + const signal_name = param.select.on; + if (isString(signal_name)) { + signals[signal_name] = param.name; + } + } + } + }); + const { datasets, ...spec } = chart; - const handleSignal = (_signalName: string, value: unknown) => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + const handleClickSignal = (signalName: string, value: unknown) => { if (id) { return onChange({ componentType: "Plot", @@ -22,12 +55,37 @@ export function Plot({ id, style, chart, onChange }: PlotProps) { }); } }; + + type SignalHandler = (signalName: string, value: unknown) => void; + + // 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 } = { + click: handleClickSignal, + }; + + const createSignalListeners = (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; + }; + + const signalListeners = createSignalListeners(signals); + return ( ); diff --git a/chartlets.js/src/lib/types/state/component.ts b/chartlets.js/src/lib/types/state/component.ts index 8b6fd8e4..d9780fdb 100644 --- a/chartlets.js/src/lib/types/state/component.ts +++ b/chartlets.js/src/lib/types/state/component.ts @@ -1,6 +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"; export type ComponentType = | "Box" @@ -54,9 +54,7 @@ export interface CheckboxState extends ComponentState { export interface PlotState extends ComponentState { type: "Plot"; chart: - | (VisualizationSpec & { - datasets?: Record; // Add the datasets property - }) + | TopLevelSpec | null; } From 19b7aec0485ea62f9266f647d9257c8a74fa4cf0 Mon Sep 17 00:00:00 2001 From: Yogesh Kumar Baljeet Singh Date: Wed, 20 Nov 2024 16:26:24 +0100 Subject: [PATCH 02/10] Fix Scale bindings warning --- chartlets.py/my_extension/my_panel_2.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/chartlets.py/my_extension/my_panel_2.py b/chartlets.py/my_extension/my_panel_2.py index 9188ab6c..6bee4c93 100644 --- a/chartlets.py/my_extension/my_panel_2.py +++ b/chartlets.py/my_extension/my_panel_2.py @@ -83,7 +83,13 @@ def make_figure( ) .properties(width=300, height=300, title="Vega charts using Shorthand syntax") .add_params(selector) - .interactive() + # .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. ) return chart From 371755f40a72aacc33f5dc5c8e7a9e5e32234eb9 Mon Sep 17 00:00:00 2001 From: Yogesh Kumar Baljeet Singh Date: Wed, 20 Nov 2024 16:27:12 +0100 Subject: [PATCH 03/10] Fix infinite extent warning --- chartlets.js/src/lib/components/Plot.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/chartlets.js/src/lib/components/Plot.tsx b/chartlets.js/src/lib/components/Plot.tsx index 1d71a8db..74c0483f 100644 --- a/chartlets.js/src/lib/components/Plot.tsx +++ b/chartlets.js/src/lib/components/Plot.tsx @@ -42,7 +42,6 @@ export function Plot({ id, style, chart, onChange }: PlotProps) { } }); - const { datasets, ...spec } = chart; // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-expect-error const handleClickSignal = (signalName: string, value: unknown) => { @@ -82,8 +81,7 @@ export function Plot({ id, style, chart, onChange }: PlotProps) { return ( Date: Wed, 20 Nov 2024 17:59:03 +0100 Subject: [PATCH 04/10] Added comments --- chartlets.js/src/lib/components/Plot.tsx | 54 ++++++++++++++----- chartlets.js/src/lib/types/state/component.ts | 4 +- 2 files changed, 43 insertions(+), 15 deletions(-) diff --git a/chartlets.js/src/lib/components/Plot.tsx b/chartlets.js/src/lib/components/Plot.tsx index 74c0483f..0c095ee1 100644 --- a/chartlets.js/src/lib/components/Plot.tsx +++ b/chartlets.js/src/lib/components/Plot.tsx @@ -6,28 +6,47 @@ import type { TopLevelParameter } from "vega-lite/src/spec/toplevel"; import type { TopLevelSelectionParameter } from "vega-lite/src/selection"; import type { Stream } from "vega-typings/types/spec/stream"; +type SignalHandler = (signalName: string, value: unknown) => void; + export interface PlotProps extends Omit { onChange: ComponentChangeHandler; } +/* +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). +*/ +function isTopLevelSelectionParameter( + param: TopLevelParameter, +): param is TopLevelSelectionParameter { + return "select" in param; +} + +/* +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. +*/ +function isString(signal_name: string | Stream): signal_name is string { + return typeof signal_name === "string"; +} + export function Plot({ id, style, chart, onChange }: PlotProps) { if (!chart) { return
; } - function isTopLevelSelectionParameter( - param: TopLevelParameter, - ): param is TopLevelSelectionParameter { - return "select" in param; - } - - function isString(signal_name: string | Stream): signal_name is string { - return typeof signal_name === "string"; - } - const signals: { [key: string]: string } = {}; + /* + Here, we loop through all the params to create map of signals which will be then used to create the map of + signal-listeners + */ chart.params?.forEach((param) => { + console.log("param", param); if (isTopLevelSelectionParameter(param)) { if ( typeof param.select === "object" && @@ -37,6 +56,12 @@ export function Plot({ id, style, chart, onChange }: PlotProps) { const signal_name = param.select.on; if (isString(signal_name)) { signals[signal_name] = param.name; + } else { + console.warn( + "The signal " + + param + + " is of Stream type (internal Vega-lite type) which is not handled yet.", + ); } } } @@ -55,13 +80,16 @@ export function Plot({ id, style, chart, onChange }: PlotProps) { } }; - type SignalHandler = (signalName: string, value: unknown) => void; - - // Currently, we only have click events support, but if more are required, they can be implemented and added in the map below. + /* + 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 } = { click: handleClickSignal, }; + /* + This function creates the map of signal listeners based on the `signals` map computed above. + */ const createSignalListeners = (signals: { [key: string]: string }) => { const signalListeners: { [key: string]: SignalHandler } = {}; Object.entries(signals).forEach(([event, signalName]) => { diff --git a/chartlets.js/src/lib/types/state/component.ts b/chartlets.js/src/lib/types/state/component.ts index d9780fdb..5a24d515 100644 --- a/chartlets.js/src/lib/types/state/component.ts +++ b/chartlets.js/src/lib/types/state/component.ts @@ -1,6 +1,6 @@ import { type CSSProperties } from "react"; import { isObject } from "@/lib/utils/isObject"; -import type {TopLevelSpec} from "vega-lite/src/spec"; +import type { TopLevelSpec } from "vega-lite/src/spec"; export type ComponentType = | "Box" @@ -54,7 +54,7 @@ export interface CheckboxState extends ComponentState { export interface PlotState extends ComponentState { type: "Plot"; chart: - | TopLevelSpec + | TopLevelSpec // This is the vega-lite specification type | null; } From 98dfd9108f515147326f1ccab516bba91eaacc82 Mon Sep 17 00:00:00 2001 From: Yogesh Kumar Baljeet Singh Date: Thu, 21 Nov 2024 10:57:58 +0100 Subject: [PATCH 05/10] Added optimizations --- chartlets.js/src/lib/components/Plot.tsx | 155 +++++++++++------------ chartlets.js/src/lib/types/state/vega.ts | 27 ++++ 2 files changed, 101 insertions(+), 81 deletions(-) create mode 100644 chartlets.js/src/lib/types/state/vega.ts diff --git a/chartlets.js/src/lib/components/Plot.tsx b/chartlets.js/src/lib/components/Plot.tsx index 0c095ee1..af2d5a17 100644 --- a/chartlets.js/src/lib/components/Plot.tsx +++ b/chartlets.js/src/lib/components/Plot.tsx @@ -2,110 +2,103 @@ import { VegaLite } from "react-vega"; import { type PlotState } from "@/lib/types/state/component"; import { type ComponentChangeHandler } from "@/lib/types/state/event"; -import type { TopLevelParameter } from "vega-lite/src/spec/toplevel"; -import type { TopLevelSelectionParameter } from "vega-lite/src/selection"; -import type { Stream } from "vega-typings/types/spec/stream"; - -type SignalHandler = (signalName: string, value: unknown) => void; +import { + isString, + isTopLevelSelectionParameter, + type SignalHandler, +} from "@/lib/types/state/vega"; +import { useCallback, useMemo } from "react"; export interface PlotProps extends Omit { onChange: ComponentChangeHandler; } -/* -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). -*/ -function isTopLevelSelectionParameter( - param: TopLevelParameter, -): param is TopLevelSelectionParameter { - return "select" in param; -} - -/* -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. -*/ -function isString(signal_name: string | Stream): signal_name is string { - return typeof signal_name === "string"; -} - export function Plot({ id, style, chart, onChange }: PlotProps) { - if (!chart) { - return
; - } - - const signals: { [key: string]: string } = {}; - /* Here, we loop through all the params to create map of signals which will be then used to create the map of signal-listeners */ - chart.params?.forEach((param) => { - console.log("param", param); - if (isTopLevelSelectionParameter(param)) { - if ( - typeof param.select === "object" && - "on" in param.select && - param.select.on != null - ) { - const signal_name = param.select.on; - if (isString(signal_name)) { - signals[signal_name] = param.name; - } else { - console.warn( - "The signal " + - param + - " is of Stream type (internal Vega-lite type) which is not handled yet.", - ); + 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; + 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]); - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-expect-error - const handleClickSignal = (signalName: string, value: unknown) => { - if (id) { - return onChange({ - componentType: "Plot", - id: id, - property: "points", - value: value, - }); - } - }; + const handleClickSignal = useCallback( + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + (signalName: string, value: unknown) => { + if (id) { + return onChange({ + componentType: "Plot", + id: id, + property: "points", + value: value, + }); + } + }, + [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 } = { - click: handleClickSignal, - }; + 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 = (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", - ); - } - }); + 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 signalListeners; - }; + const signalListeners = useMemo( + () => createSignalListeners(signals), + [createSignalListeners, signals], + ); - const signalListeners = createSignalListeners(signals); + if (!chart) { + return
; + } return ( 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; +} + +/* +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. +*/ +export function isString(signal_name: string | Stream): signal_name is string { + return typeof signal_name === "string"; +} From baef8eeecbaa3f1e14b8fa990908aff6d6f5ac96 Mon Sep 17 00:00:00 2001 From: Yogesh Kumar Baljeet Singh Date: Thu, 21 Nov 2024 15:57:37 +0100 Subject: [PATCH 06/10] Changes based on reviewers comments --- chartlets.js/src/lib/components/Plot.tsx | 89 +----------------- chartlets.js/src/lib/hooks.ts | 109 ++++++++++++++++++++++- chartlets.js/src/lib/types/state/vega.ts | 22 ++--- chartlets.js/src/lib/utils/isString.ts | 3 + 4 files changed, 119 insertions(+), 104 deletions(-) create mode 100644 chartlets.js/src/lib/utils/isString.ts diff --git a/chartlets.js/src/lib/components/Plot.tsx b/chartlets.js/src/lib/components/Plot.tsx index af2d5a17..ef21832c 100644 --- a/chartlets.js/src/lib/components/Plot.tsx +++ b/chartlets.js/src/lib/components/Plot.tsx @@ -2,99 +2,14 @@ import { VegaLite } from "react-vega"; import { type PlotState } from "@/lib/types/state/component"; import { type ComponentChangeHandler } from "@/lib/types/state/event"; -import { - isString, - isTopLevelSelectionParameter, - type SignalHandler, -} from "@/lib/types/state/vega"; -import { useCallback, useMemo } from "react"; +import { useSignalListeners } from "@/lib/hooks"; export interface PlotProps extends Omit { onChange: ComponentChangeHandler; } export function Plot({ id, style, chart, onChange }: PlotProps) { - /* - Here, we loop through all the params to create map of signals which will be then used to create the map of - signal-listeners - */ - 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; - 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( - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-expect-error - (signalName: string, value: unknown) => { - if (id) { - return onChange({ - componentType: "Plot", - id: id, - property: "points", - value: value, - }); - } - }, - [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], - ); - - const signalListeners = useMemo( - () => createSignalListeners(signals), - [createSignalListeners, signals], - ); + const signalListeners = useSignalListeners(chart, id, onChange); if (!chart) { return
; diff --git a/chartlets.js/src/lib/hooks.ts b/chartlets.js/src/lib/hooks.ts index c23daf1f..89aa575f 100644 --- a/chartlets.js/src/lib/hooks.ts +++ b/chartlets.js/src/lib/hooks.ts @@ -1,7 +1,14 @@ import type { StoreState } from "@/lib/types/state/store"; import { store } from "@/lib/store"; -import { useMemo } from "react"; +import { useCallback, useMemo } from "react"; import type { ContributionState } from "@/lib/types/state/contribution"; +import type { ComponentChangeHandler } from "@/lib/types/state/event"; +import { + isTopLevelSelectionParameter, + type SignalHandler, +} from "@/lib/types/state/vega"; +import type { TopLevelSpec } from "vega-lite/src/spec"; +import { isString } from "@/lib/utils/isString"; const selectConfiguration = (state: StoreState) => state.configuration; @@ -32,3 +39,103 @@ export function makeContributionsHook( }, [contributions]); }; } + +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( + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + (signalName: string, value: unknown) => { + if (id) { + return onChange({ + componentType: "Plot", + id: id, + property: "points", + value: value, + }); + } + }, + [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], + ); +} diff --git a/chartlets.js/src/lib/types/state/vega.ts b/chartlets.js/src/lib/types/state/vega.ts index 1b730c0e..73f0cace 100644 --- a/chartlets.js/src/lib/types/state/vega.ts +++ b/chartlets.js/src/lib/types/state/vega.ts @@ -1,27 +1,17 @@ import type { TopLevelParameter } from "vega-lite/src/spec/toplevel"; import type { TopLevelSelectionParameter } from "vega-lite/src/selection"; -import type { Stream } from "vega-typings/types/spec/stream"; 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). -*/ + 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; } - -/* -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. -*/ -export function isString(signal_name: string | Stream): signal_name is string { - return typeof signal_name === "string"; -} diff --git a/chartlets.js/src/lib/utils/isString.ts b/chartlets.js/src/lib/utils/isString.ts new file mode 100644 index 00000000..9be9b393 --- /dev/null +++ b/chartlets.js/src/lib/utils/isString.ts @@ -0,0 +1,3 @@ +export function isString(signalName: unknown): signalName is string { + return typeof signalName === "string"; +} From 921c50f10e329b35a25dc57f532c775d6a657da6 Mon Sep 17 00:00:00 2001 From: Yogesh Kumar Baljeet Singh Date: Thu, 21 Nov 2024 16:26:31 +0100 Subject: [PATCH 07/10] Replace == "string" with isString type-guard --- chartlets.js/src/lib/components/Select.tsx | 3 ++- chartlets.js/src/lib/types/state/component.ts | 3 ++- chartlets.js/src/lib/utils/objPath.ts | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/chartlets.js/src/lib/components/Select.tsx b/chartlets.js/src/lib/components/Select.tsx index 24610abd..d238b64c 100644 --- a/chartlets.js/src/lib/components/Select.tsx +++ b/chartlets.js/src/lib/components/Select.tsx @@ -8,6 +8,7 @@ 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; @@ -64,7 +65,7 @@ export function Select({ function normalizeSelectOption( option: SelectOption, ): [string | number, string] { - if (typeof option === "string") { + if (isString(option)) { return [option, option]; } else if (typeof option === "number") { return [option, option.toString()]; diff --git a/chartlets.js/src/lib/types/state/component.ts b/chartlets.js/src/lib/types/state/component.ts index 5a24d515..9bc53829 100644 --- a/chartlets.js/src/lib/types/state/component.ts +++ b/chartlets.js/src/lib/types/state/component.ts @@ -1,6 +1,7 @@ import { type CSSProperties } from "react"; import { isObject } from "@/lib/utils/isObject"; import type { TopLevelSpec } from "vega-lite/src/spec"; +import { isString } from "@/lib/utils/isString"; export type ComponentType = | "Box" @@ -67,7 +68,7 @@ export interface TypographyState extends ContainerState { } export function isComponentState(object: unknown): object is ComponentState { - return isObject(object) && typeof object.type === "string"; + return isObject(object) && isString(object.type); } export function isContainerState(object: unknown): object is ContainerState { diff --git a/chartlets.js/src/lib/utils/objPath.ts b/chartlets.js/src/lib/utils/objPath.ts index c335f8a4..45156f9a 100644 --- a/chartlets.js/src/lib/utils/objPath.ts +++ b/chartlets.js/src/lib/utils/objPath.ts @@ -1,4 +1,5 @@ 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; @@ -83,7 +84,7 @@ export function normalizeObjPath(pathLike: ObjPathLike): ObjPath { } export function formatObjPath(objPath: ObjPathLike): string { - if (typeof objPath === "string") { + if (isString(objPath)) { return objPath; } else if (Array.isArray(objPath)) { return objPath From 82c92eb7886ec028d4916d2ca5238e8044fd049e Mon Sep 17 00:00:00 2001 From: Yogesh Kumar Baljeet Singh Date: Fri, 22 Nov 2024 09:36:01 +0100 Subject: [PATCH 08/10] TODO: new callback onclick test --- chartlets.js/src/lib/hooks.ts | 8 +++----- chartlets.py/my_extension/my_panel_1.py | 12 +++++++++++- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/chartlets.js/src/lib/hooks.ts b/chartlets.js/src/lib/hooks.ts index 89aa575f..52a91e7d 100644 --- a/chartlets.js/src/lib/hooks.ts +++ b/chartlets.js/src/lib/hooks.ts @@ -86,15 +86,13 @@ export function useSignalListeners( }, [chart]); const handleClickSignal = useCallback( - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-expect-error - (signalName: string, value: unknown) => { + (signalName: string, signalValue: unknown) => { if (id) { return onChange({ componentType: "Plot", id: id, - property: "points", - value: value, + property: signalName, + value: signalValue, }); } }, diff --git a/chartlets.py/my_extension/my_panel_1.py b/chartlets.py/my_extension/my_panel_1.py index d50822e5..ea4f7898 100644 --- a/chartlets.py/my_extension/my_panel_1.py +++ b/chartlets.py/my_extension/my_panel_1.py @@ -60,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="onClick", fields=["x", variable_name] + on="click", name="points", 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. @@ -81,3 +81,13 @@ def make_figure(ctx: Context, selected_dataset: int = 0) -> alt.Chart: ) return chart + + +# # TODO: see if we can get the value of the clicked points from the click params above +# @panel.callback( +# Input("selected_dataset"), +# Output("plot", "chart"), +# ) +# def test_callback( +# self, +# ): ... From efdc7d32fc4782791239da9b5e772227a0b5702a Mon Sep 17 00:00:00 2001 From: Yogesh Kumar Baljeet Singh Date: Fri, 22 Nov 2024 12:25:33 +0100 Subject: [PATCH 09/10] Access the click event in the callback function --- chartlets.py/my_extension/my_panel_1.py | 54 ++++++++++++++++++++----- 1 file changed, 44 insertions(+), 10 deletions(-) diff --git a/chartlets.py/my_extension/my_panel_1.py b/chartlets.py/my_extension/my_panel_1.py index ea4f7898..9a6af188 100644 --- a/chartlets.py/my_extension/my_panel_1.py +++ b/chartlets.py/my_extension/my_panel_1.py @@ -1,6 +1,10 @@ +import copy +from types import NoneType + import altair as alt +import pandas as pd -from chartlets import Component, Input, Output +from chartlets import Component, Input, Output, State from chartlets.components import Plot, Box, Select from chartlets.demo.contribs import Panel from chartlets.demo.context import Context @@ -79,15 +83,45 @@ 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 -# # TODO: see if we can get the value of the clicked points from the click params above -# @panel.callback( -# Input("selected_dataset"), -# Output("plot", "chart"), -# ) -# def test_callback( -# self, -# ): ... +@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) From c346d19d66212c506101b3c0bedbaf2ee189ea28 Mon Sep 17 00:00:00 2001 From: Yogesh Kumar Baljeet Singh Date: Fri, 22 Nov 2024 12:39:20 +0100 Subject: [PATCH 10/10] fix merge conflict error --- chartlets.js/src/lib/hooks.ts | 36 ++++++++++++++++++----------------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/chartlets.js/src/lib/hooks.ts b/chartlets.js/src/lib/hooks.ts index 669815ec..dfbf932e 100644 --- a/chartlets.js/src/lib/hooks.ts +++ b/chartlets.js/src/lib/hooks.ts @@ -50,11 +50,11 @@ export function useSignalListeners( 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. + 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 {}; @@ -68,12 +68,12 @@ export function useSignalListeners( ) { 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. + 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; @@ -104,9 +104,9 @@ export function useSignalListeners( ); /* - Currently, we only have click events support, but if more are required, - they can be implemented and added in the map below. - */ + 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, @@ -115,9 +115,9 @@ export function useSignalListeners( ); /* - This function creates the map of signal listeners based on the `signals` - map computed above. - */ + 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 } = {}; @@ -139,6 +139,8 @@ export function useSignalListeners( return useMemo( () => createSignalListeners(signals), [createSignalListeners, signals], + ); +} /** * A hook that retrieves the contributions for the given contribution