From 4ddc6d11a86b074ab25c710b407dfb339f39fb17 Mon Sep 17 00:00:00 2001 From: Yogesh Kumar Baljeet Singh Date: Thu, 24 Oct 2024 18:28:31 +0200 Subject: [PATCH 1/7] Initial toolbar implementation - not clean --- dashi/package-lock.json | 60 +++++++++++++----------- dashi/package.json | 2 + dashi/src/lib/components/DashiPlot.tsx | 63 ++++++++++++++++++++++---- dashi/src/lib/types/state/component.ts | 4 +- dashipy/my_extension/my_panel_1.py | 6 +-- dashipy/my_extension/my_panel_2.py | 2 +- 6 files changed, 97 insertions(+), 40 deletions(-) diff --git a/dashi/package-lock.json b/dashi/package-lock.json index fa543d05..0c7c4db8 100644 --- a/dashi/package-lock.json +++ b/dashi/package-lock.json @@ -12,11 +12,13 @@ "@emotion/react": "^11.13.3", "@emotion/styled": "^11.13.0", "@fontsource/roboto": "^5.1.0", + "@mui/icons-material": "^6.1.4", "@mui/material": "^6.1.5", "microdiff": "^1.4.0", "react": "^18.3.1", "react-dom": "^18.3.1", "react-vega": "^7.6.0", + "vega-lite": "^5.21.0", "zustand": "^5.0.0" }, "devDependencies": { @@ -1210,6 +1212,32 @@ "url": "https://opencollective.com/mui-org" } }, + "node_modules/@mui/icons-material": { + "version": "6.1.5", + "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-6.1.5.tgz", + "integrity": "sha512-SbxFtO5I4cXfvhjAMgGib/t2lQUzcEzcDFYiRHRufZUeMMeXuoKaGsptfwAHTepYkv0VqcCwvxtvtWbpZLAbjQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.25.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@mui/material": "^6.1.5", + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@mui/material": { "version": "6.1.5", "resolved": "https://registry.npmjs.org/@mui/material/-/material-6.1.5.tgz", @@ -2917,7 +2945,6 @@ "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", "license": "ISC", - "peer": true, "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", @@ -2932,7 +2959,6 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "license": "MIT", - "peer": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -2948,7 +2974,6 @@ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "license": "MIT", - "peer": true, "dependencies": { "color-name": "~1.1.4" }, @@ -2960,22 +2985,19 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/cliui/node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/cliui/node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "license": "MIT", - "peer": true, "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -2990,7 +3012,6 @@ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "license": "MIT", - "peer": true, "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -3516,7 +3537,6 @@ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "license": "MIT", - "peer": true, "engines": { "node": ">=6" } @@ -4057,7 +4077,6 @@ "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "license": "ISC", - "peer": true, "engines": { "node": "6.* || 8.* || >= 10.*" } @@ -5373,7 +5392,6 @@ "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -6344,15 +6362,13 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/vega-event-selector/-/vega-event-selector-3.0.1.tgz", "integrity": "sha512-K5zd7s5tjr1LiOOkjGpcVls8GsH/f2CWCrWcpKy74gTCp+llCdwz0Enqo013ZlGaRNjfgD/o1caJRt3GSaec4A==", - "license": "BSD-3-Clause", - "peer": true + "license": "BSD-3-Clause" }, "node_modules/vega-expression": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/vega-expression/-/vega-expression-5.1.1.tgz", "integrity": "sha512-zv9L1Hm0KHE9M7mldHyz8sXbGu3KmC0Cdk7qfHkcTNS75Jpsem6jkbu6ZAwx5cNUeW91AxUQOu77r4mygq2wUQ==", "license": "BSD-3-Clause", - "peer": true, "dependencies": { "@types/estree": "^1.0.0", "vega-util": "^1.17.2" @@ -6570,7 +6586,6 @@ "resolved": "https://registry.npmjs.org/vega-lite/-/vega-lite-5.21.0.tgz", "integrity": "sha512-hNxM9nuMqpI1vkUOhEx6ewEf23WWLmJxSFJ4TA86AW43ixJyqcLV+iSCO0NipuVTE0rlDcc2e8joSewWyOlEwA==", "license": "BSD-3-Clause", - "peer": true, "dependencies": { "json-stringify-pretty-compact": "~3.0.0", "tslib": "~2.6.3", @@ -6596,15 +6611,13 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/json-stringify-pretty-compact/-/json-stringify-pretty-compact-3.0.0.tgz", "integrity": "sha512-Rc2suX5meI0S3bfdZuA7JMFBGkJ875ApfVyq2WHELjBiiG22My/l7/8zPpH/CfFVQHuVLd8NLR0nv6vi0BYYKA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/vega-lite/node_modules/tslib": { "version": "2.6.3", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==", - "license": "0BSD", - "peer": true + "license": "0BSD" }, "node_modules/vega-loader": { "version": "4.5.2", @@ -7537,7 +7550,6 @@ "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", "license": "ISC", - "peer": true, "engines": { "node": ">=10" } @@ -7563,7 +7575,6 @@ "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", "license": "MIT", - "peer": true, "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", @@ -7582,7 +7593,6 @@ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", "license": "ISC", - "peer": true, "engines": { "node": ">=12" } @@ -7591,15 +7601,13 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/yargs/node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "license": "MIT", - "peer": true, "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", diff --git a/dashi/package.json b/dashi/package.json index f89c421c..8cfadec1 100644 --- a/dashi/package.json +++ b/dashi/package.json @@ -49,10 +49,12 @@ "@emotion/styled": "^11.13.0", "@fontsource/roboto": "^5.1.0", "@mui/material": "^6.1.5", + "@mui/icons-material": "^6.1.4", "microdiff": "^1.4.0", "react": "^18.3.1", "react-dom": "^18.3.1", "react-vega": "^7.6.0", + "vega-lite": "^5.21.0", "zustand": "^5.0.0" }, "peerDependencies": { diff --git a/dashi/src/lib/components/DashiPlot.tsx b/dashi/src/lib/components/DashiPlot.tsx index c4af48b3..bdf532a6 100644 --- a/dashi/src/lib/components/DashiPlot.tsx +++ b/dashi/src/lib/components/DashiPlot.tsx @@ -1,7 +1,10 @@ -import { VegaLite } from "react-vega"; +import { VegaLite, type VisualizationSpec } from "react-vega"; import { type PlotState } from "@/lib/types/state/component"; import { type PropertyChangeHandler } from "@/lib/types/model/event"; +import { useEffect, useState } from "react"; +import type { TopLevelParameter } from "vega-lite/build/src/spec/toplevel"; +import Button from "@mui/material/Button"; export interface DashiPlotProps extends Omit { onPropertyChange: PropertyChangeHandler; @@ -13,10 +16,47 @@ export function DashiPlot({ chart, onPropertyChange, }: DashiPlotProps) { + const [updatedSpec, setupdatedSpec] = useState< + | (VisualizationSpec & { + params?: TopLevelParameter[]; + }) + | null + >(); + const enableClickMode = () => { + const panAndZoom: { params: TopLevelParameter[] } = { + params: [ + { + name: "grid", + select: "interval", + bind: "scales", + }, + ], + }; + const spec = { + ...updatedSpec, + params: [...(updatedSpec?.params || []), ...panAndZoom.params], + }; + setupdatedSpec(spec); + }; + + useEffect(() => { + if (chart) { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { datasets, ...specification } = chart; + setupdatedSpec(specification); + } + }, [chart]); + if (!chart) { return
; } - const { datasets, ...spec } = chart; + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { datasets } = chart; + if (!updatedSpec) { + return null; + } + console.log("spec::", updatedSpec.params, updatedSpec); const handleSignal = (_signalName: string, value: unknown) => { if (id) { return onPropertyChange({ @@ -28,12 +68,17 @@ export function DashiPlot({ } }; return ( - + <> + + + + ); } diff --git a/dashi/src/lib/types/state/component.ts b/dashi/src/lib/types/state/component.ts index 8b4020ab..26048e9d 100644 --- a/dashi/src/lib/types/state/component.ts +++ b/dashi/src/lib/types/state/component.ts @@ -1,5 +1,6 @@ import { type CSSProperties } from "react"; import type { VisualizationSpec } from "react-vega"; +import type { TopLevelParameter } from "vega-lite/src/spec/toplevel"; export type ComponentType = "Button" | "Checkbox" | "Dropdown" | "Plot" | "Box"; @@ -37,9 +38,10 @@ export interface CheckboxState extends ComponentState { export interface PlotState extends ComponentState { type: "Plot"; - chart: + chart?: | (VisualizationSpec & { datasets?: Record; // Add the datasets property + params?: TopLevelParameter[]; }) | null; } diff --git a/dashipy/my_extension/my_panel_1.py b/dashipy/my_extension/my_panel_1.py index 552855be..b46d5396 100644 --- a/dashipy/my_extension/my_panel_1.py +++ b/dashipy/my_extension/my_panel_1.py @@ -60,7 +60,7 @@ def make_figure(ctx: Context, selected_dataset: int = 0) -> alt.Chart: fields=["a", "b"]) # 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. - chart = alt.Chart(dataset).mark_bar(cornerRadius=corner_var).encode( + chart = alt.Chart(dataset).mark_bar(clip=False, cornerRadius=22).encode( x=alt.X('a:N', title='a'), y=alt.Y('b:Q', title='b'), tooltip=[ @@ -71,7 +71,7 @@ def make_figure(ctx: Context, selected_dataset: int = 0) -> alt.Chart: ).properties( width=300, height=300, - title="Vega charts" - ).add_params(corner_var, click_param) + title="Vega charts", + ).add_params(click_param) return chart diff --git a/dashipy/my_extension/my_panel_2.py b/dashipy/my_extension/my_panel_2.py index 95c6b30c..ce230618 100644 --- a/dashipy/my_extension/my_panel_2.py +++ b/dashipy/my_extension/my_panel_2.py @@ -70,6 +70,6 @@ def make_figure(ctx: Context, selected_dataset: int = 0) -> alt.Chart: title="Vega charts using Shorthand syntax" ).add_params( selector - ).interactive() + ) return chart From 1e9046ae70015c84db9e7e0b9b3696afef754036 Mon Sep 17 00:00:00 2001 From: Yogesh Kumar Baljeet Singh Date: Fri, 25 Oct 2024 17:33:14 +0200 Subject: [PATCH 2/7] Refactored Toolbar into a new component --- dashi/src/demo/components/Panel.tsx | 14 +- dashi/src/demo/components/PanelsRow.tsx | 1 + dashi/src/lib/components/DashiBox.tsx | 3 + dashi/src/lib/components/DashiButton.tsx | 1 + dashi/src/lib/components/DashiChildren.tsx | 5 +- dashi/src/lib/components/DashiComponent.tsx | 1 + dashi/src/lib/components/DashiDropdown.tsx | 2 + dashi/src/lib/components/DashiPlot.tsx | 60 ++---- dashi/src/lib/components/DashiPlotToolbar.tsx | 173 ++++++++++++++++++ dashi/src/lib/types/state/component.ts | 14 +- dashipy/my_extension/my_panel_1.py | 6 +- dashipy/my_extension/my_panel_2.py | 2 +- 12 files changed, 220 insertions(+), 62 deletions(-) create mode 100644 dashi/src/lib/components/DashiPlotToolbar.tsx diff --git a/dashi/src/demo/components/Panel.tsx b/dashi/src/demo/components/Panel.tsx index 804c554d..2c26ae8c 100644 --- a/dashi/src/demo/components/Panel.tsx +++ b/dashi/src/demo/components/Panel.tsx @@ -34,11 +34,17 @@ const panelContentStyle: CSSProperties = { interface PanelProps { panelModel: Contribution; + panelIndex: number; panelState: ContributionState; onPropertyChange: PropertyChangeHandler; } -function Panel({ panelModel, panelState, onPropertyChange }: PanelProps) { +function Panel({ + panelModel, + panelIndex, + panelState, + onPropertyChange, +}: PanelProps) { if (!panelState.visible) { return null; } @@ -47,7 +53,11 @@ function Panel({ panelModel, panelState, onPropertyChange }: PanelProps) { const componentModelResult = panelState.componentStateResult; if (componentModelResult.data && componentState) { panelElement = ( - + ); } else if (componentModelResult.error) { panelElement = ( diff --git a/dashi/src/demo/components/PanelsRow.tsx b/dashi/src/demo/components/PanelsRow.tsx index d0e0c2f1..c301d127 100644 --- a/dashi/src/demo/components/PanelsRow.tsx +++ b/dashi/src/demo/components/PanelsRow.tsx @@ -38,6 +38,7 @@ function PanelsRow() { key={panelIndex} panelState={panelState} panelModel={panelModels[panelIndex]} + panelIndex={panelIndex} onPropertyChange={(e) => handlePropertyChange(panelIndex, e)} />, ); diff --git a/dashi/src/lib/components/DashiBox.tsx b/dashi/src/lib/components/DashiBox.tsx index c0444c34..7d3ffdc4 100644 --- a/dashi/src/lib/components/DashiBox.tsx +++ b/dashi/src/lib/components/DashiBox.tsx @@ -6,17 +6,20 @@ import { DashiChildren } from "./DashiChildren"; export interface DashiBoxProps extends Omit { onPropertyChange: PropertyChangeHandler; + panelIndex: number; } export function DashiBox({ id, style, components, + panelIndex, onPropertyChange, }: DashiBoxProps) { return ( diff --git a/dashi/src/lib/components/DashiButton.tsx b/dashi/src/lib/components/DashiButton.tsx index 13b98c55..e8a1d4e8 100644 --- a/dashi/src/lib/components/DashiButton.tsx +++ b/dashi/src/lib/components/DashiButton.tsx @@ -6,6 +6,7 @@ import { type PropertyChangeHandler } from "@/lib/types/model/event"; export interface DashiButtonProps extends Omit { onPropertyChange: PropertyChangeHandler; + panelIndex: number; } export function DashiButton({ diff --git a/dashi/src/lib/components/DashiChildren.tsx b/dashi/src/lib/components/DashiChildren.tsx index 84008fd7..0293342d 100644 --- a/dashi/src/lib/components/DashiChildren.tsx +++ b/dashi/src/lib/components/DashiChildren.tsx @@ -1,14 +1,16 @@ import { type PropertyChangeHandler } from "@/lib/types/model/event"; import { type ComponentState } from "@/lib/types/state/component"; -import { DashiComponent } from "./DashiComponent"; +import { DashiComponent } from "@/lib"; export interface DashiChildrenProps { components?: ComponentState[]; onPropertyChange: PropertyChangeHandler; + panelIndex: number; } export function DashiChildren({ components, + panelIndex, onPropertyChange, }: DashiChildrenProps) { if (!components || components.length === 0) { @@ -22,6 +24,7 @@ export function DashiChildren({ ); diff --git a/dashi/src/lib/components/DashiComponent.tsx b/dashi/src/lib/components/DashiComponent.tsx index a2b3737f..21ad12a6 100644 --- a/dashi/src/lib/components/DashiComponent.tsx +++ b/dashi/src/lib/components/DashiComponent.tsx @@ -7,6 +7,7 @@ import { DashiDropdown, type DashiDropdownProps } from "./DashiDropdown"; export interface DashiComponentProps extends ComponentState { onPropertyChange: PropertyChangeHandler; + panelIndex: number; } export function DashiComponent({ type, ...props }: DashiComponentProps) { diff --git a/dashi/src/lib/components/DashiDropdown.tsx b/dashi/src/lib/components/DashiDropdown.tsx index 8a3de05c..07bbcc04 100644 --- a/dashi/src/lib/components/DashiDropdown.tsx +++ b/dashi/src/lib/components/DashiDropdown.tsx @@ -8,6 +8,7 @@ import { type PropertyChangeHandler } from "@/lib/types/model/event"; export interface DashiDropdownProps extends Omit { onPropertyChange: PropertyChangeHandler; + panelIndex: number; } export function DashiDropdown({ @@ -46,6 +47,7 @@ export function DashiDropdown({ value={`${value}`} disabled={disabled} onChange={handleChange} + variant={"standard"} > {options.map(([text, value], index) => ( diff --git a/dashi/src/lib/components/DashiPlot.tsx b/dashi/src/lib/components/DashiPlot.tsx index bdf532a6..ba090ad8 100644 --- a/dashi/src/lib/components/DashiPlot.tsx +++ b/dashi/src/lib/components/DashiPlot.tsx @@ -1,62 +1,26 @@ -import { VegaLite, type VisualizationSpec } from "react-vega"; +import { VegaLite } from "react-vega"; +import { type PropertyChangeHandler } from "@/lib/types/model/event"; import { type PlotState } from "@/lib/types/state/component"; -import { type PropertyChangeHandler } from "@/lib/types/model/event"; -import { useEffect, useState } from "react"; -import type { TopLevelParameter } from "vega-lite/build/src/spec/toplevel"; -import Button from "@mui/material/Button"; +import { DashiPlotToolbar } from "@/lib/components/DashiPlotToolbar"; export interface DashiPlotProps extends Omit { onPropertyChange: PropertyChangeHandler; + panelIndex: number; } export function DashiPlot({ id, style, chart, + panelIndex, onPropertyChange, }: DashiPlotProps) { - const [updatedSpec, setupdatedSpec] = useState< - | (VisualizationSpec & { - params?: TopLevelParameter[]; - }) - | null - >(); - const enableClickMode = () => { - const panAndZoom: { params: TopLevelParameter[] } = { - params: [ - { - name: "grid", - select: "interval", - bind: "scales", - }, - ], - }; - const spec = { - ...updatedSpec, - params: [...(updatedSpec?.params || []), ...panAndZoom.params], - }; - setupdatedSpec(spec); - }; - - useEffect(() => { - if (chart) { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { datasets, ...specification } = chart; - setupdatedSpec(specification); - } - }, [chart]); - if (!chart) { return
; } + const { datasets, ...specification } = chart; - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { datasets } = chart; - if (!updatedSpec) { - return null; - } - console.log("spec::", updatedSpec.params, updatedSpec); const handleSignal = (_signalName: string, value: unknown) => { if (id) { return onPropertyChange({ @@ -68,17 +32,19 @@ export function DashiPlot({ } }; return ( - <> - - + - + ); } diff --git a/dashi/src/lib/components/DashiPlotToolbar.tsx b/dashi/src/lib/components/DashiPlotToolbar.tsx new file mode 100644 index 00000000..fa974c72 --- /dev/null +++ b/dashi/src/lib/components/DashiPlotToolbar.tsx @@ -0,0 +1,173 @@ +import React, { type CSSProperties, useState } from "react"; +import type { TopLevelParameter } from "vega-lite/build/src/spec/toplevel"; +import LoupeIcon from "@mui/icons-material/Loupe"; +import ReplayIcon from "@mui/icons-material/Replay"; +import HighlightAltIcon from "@mui/icons-material/HighlightAlt"; +import { Tooltip } from "@mui/material"; +import { type PropertyChangeHandler } from "@/lib"; +import type { PlotState } from "@/lib/types/state/component"; +import { store } from "@/lib/store"; + +export interface DashiPlotToolbarProps { + style: CSSProperties; + children: React.ReactNode; + panelIndex: number; + onPropertyChange: PropertyChangeHandler; +} + +export function DashiPlotToolbar({ + style, + children, + panelIndex, + onPropertyChange, +}: DashiPlotToolbarProps) { + const [showTooltip, setShowTooltip] = useState(false); + + const handleMouseEnter = () => setShowTooltip(true); + const handleMouseLeave = () => setShowTooltip(false); + + const getLatestChart = () => { + const { contributionStatesRecord } = store.getState(); + const contribPoint = "panels"; + const contributionState = + contributionStatesRecord[contribPoint][panelIndex]; + const plot = contributionState?.componentState?.components?.find( + (component) => component.type === "Plot", + ) as PlotState; + return plot?.chart; + }; + + let chart = getLatestChart(); + + // TODO: Try to memoize the following + // const chart = useMemo(() => { + // const plot = contributionState?.componentState?.components?.find( + // (component) => component.type === "Plot", + // ) as PlotState; + // return plot?.chart; + // }, [contributionState]); + + // const parameters = useMemo( + // () => + // chart?.params?.reduce((map, param, paramIndex) => { + // map.set(param.name, paramIndex); + // return map; + // }, new Map()), + // [chart?.params], + // ); + + if (!chart) { + return null; + } + + const enablePanAndZoomMode = () => { + resetMode(); + chart = getLatestChart(); + console.log("chart in zoom::", chart); + const panAndZoom: { params: TopLevelParameter[] } = { + params: [ + { + name: "grid", + select: "interval", + bind: "scales", + }, + ], + }; + + if (!chart?.params?.find((param) => param.name === "grid")) { + const updatedSpec = { + ...chart, + params: [...(chart?.params || []), ...panAndZoom.params], + }; + console.log("updatedSpec", updatedSpec); + return onPropertyChange({ + componentType: "Plot", + componentId: "plot", + propertyName: "chart", + propertyValue: updatedSpec, + }); + } + }; + + const resetMode = () => { + const filteredParams = chart?.params?.filter( + (param) => !(param.name === "grid" || param.name === "brush"), + ); + + if (filteredParams !== chart?.params) { + const spec = { + ...chart, + params: filteredParams, + }; + return onPropertyChange({ + componentType: "Plot", + componentId: "plot", + propertyName: "chart", + propertyValue: spec, + }); + } + }; + + const enableBrushMode = () => { + resetMode(); + chart = getLatestChart(); + console.log("chart in brush:", chart); + const brush: { params: TopLevelParameter[] } = { + params: [ + { + name: "brush", + select: "interval", + }, + ], + }; + + if (!chart?.params?.find((param) => param.name === "brush")) { + const updatedSpec = { + ...chart, + params: [...(chart?.params || []), ...brush.params], + }; + return onPropertyChange({ + componentType: "Plot", + componentId: "plot", + propertyName: "chart", + propertyValue: updatedSpec, + }); + } + }; + + return ( +
+ {showTooltip && ( +
+ + + + + + + + + +
+ )} + {children} +
+ ); +} diff --git a/dashi/src/lib/types/state/component.ts b/dashi/src/lib/types/state/component.ts index 26048e9d..873f9ffd 100644 --- a/dashi/src/lib/types/state/component.ts +++ b/dashi/src/lib/types/state/component.ts @@ -38,14 +38,16 @@ export interface CheckboxState extends ComponentState { export interface PlotState extends ComponentState { type: "Plot"; - chart?: - | (VisualizationSpec & { - datasets?: Record; // Add the datasets property - params?: TopLevelParameter[]; - }) - | null; + chart?: Specification; } +export type Specification = + | (VisualizationSpec & { + datasets?: Record; + params?: TopLevelParameter[]; + }) + | null; + export interface BoxState extends ContainerState { type: "Box"; } diff --git a/dashipy/my_extension/my_panel_1.py b/dashipy/my_extension/my_panel_1.py index b46d5396..a77950ee 100644 --- a/dashipy/my_extension/my_panel_1.py +++ b/dashipy/my_extension/my_panel_1.py @@ -50,17 +50,13 @@ def render_panel(ctx: Context) -> Component: def make_figure(ctx: Context, selected_dataset: int = 0) -> alt.Chart: dataset = ctx.datasets[selected_dataset] - # Create a slider - corner_slider = alt.binding_range(min=0, max=50, step=1) - # Create a parameter and bind that to the slider - corner_var = alt.param(bind=corner_slider, value=0, name="cornerRadius") # 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=["a", "b"]) # 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. - chart = alt.Chart(dataset).mark_bar(clip=False, cornerRadius=22).encode( + chart = alt.Chart(dataset).mark_bar().encode( x=alt.X('a:N', title='a'), y=alt.Y('b:Q', title='b'), tooltip=[ diff --git a/dashipy/my_extension/my_panel_2.py b/dashipy/my_extension/my_panel_2.py index ce230618..61562ad5 100644 --- a/dashipy/my_extension/my_panel_2.py +++ b/dashipy/my_extension/my_panel_2.py @@ -67,7 +67,7 @@ def make_figure(ctx: Context, selected_dataset: int = 0) -> alt.Chart: ).properties( width=300, height=300, - title="Vega charts using Shorthand syntax" + title="Vega charts using Shorthand syntax", ).add_params( selector ) From 36e9b878900973d01ba57bf47d6fae477586daea Mon Sep 17 00:00:00 2001 From: Yogesh Kumar Baljeet Singh Date: Mon, 28 Oct 2024 09:58:02 +0100 Subject: [PATCH 3/7] Extract toolbarspec into enum --- dashi/src/lib/components/DashiPlotToolbar.tsx | 52 ++++++++++++------- 1 file changed, 33 insertions(+), 19 deletions(-) diff --git a/dashi/src/lib/components/DashiPlotToolbar.tsx b/dashi/src/lib/components/DashiPlotToolbar.tsx index fa974c72..c81a97f2 100644 --- a/dashi/src/lib/components/DashiPlotToolbar.tsx +++ b/dashi/src/lib/components/DashiPlotToolbar.tsx @@ -60,26 +60,48 @@ export function DashiPlotToolbar({ return null; } + enum Toolbar { + PanAndZoom, + Brush, + } + + const getToolbarSpec = ( + toolbar: Toolbar, + ): { params: TopLevelParameter[] } => { + switch (toolbar) { + case Toolbar.PanAndZoom: + return { + params: [ + { + name: "grid", + select: "interval", + bind: "scales", + }, + ], + }; + case Toolbar.Brush: + return { + params: [ + { + name: "brush", + select: "interval", + }, + ], + }; + } + }; + const enablePanAndZoomMode = () => { resetMode(); chart = getLatestChart(); console.log("chart in zoom::", chart); - const panAndZoom: { params: TopLevelParameter[] } = { - params: [ - { - name: "grid", - select: "interval", - bind: "scales", - }, - ], - }; + const panAndZoom = getToolbarSpec(Toolbar.PanAndZoom); if (!chart?.params?.find((param) => param.name === "grid")) { const updatedSpec = { ...chart, params: [...(chart?.params || []), ...panAndZoom.params], }; - console.log("updatedSpec", updatedSpec); return onPropertyChange({ componentType: "Plot", componentId: "plot", @@ -111,15 +133,7 @@ export function DashiPlotToolbar({ const enableBrushMode = () => { resetMode(); chart = getLatestChart(); - console.log("chart in brush:", chart); - const brush: { params: TopLevelParameter[] } = { - params: [ - { - name: "brush", - select: "interval", - }, - ], - }; + const brush = getToolbarSpec(Toolbar.Brush); if (!chart?.params?.find((param) => param.name === "brush")) { const updatedSpec = { From b28bdd41e987afaab2edca8ecdb6416300dc4bff Mon Sep 17 00:00:00 2001 From: Yogesh Kumar Baljeet Singh Date: Mon, 28 Oct 2024 15:13:25 +0100 Subject: [PATCH 4/7] Add box-zoom option (currently only works for bar chart) --- dashi/src/lib/components/DashiPlotToolbar.tsx | 116 ++++++++++++++---- dashi/src/lib/types/state/component.ts | 1 + 2 files changed, 91 insertions(+), 26 deletions(-) diff --git a/dashi/src/lib/components/DashiPlotToolbar.tsx b/dashi/src/lib/components/DashiPlotToolbar.tsx index c81a97f2..2ce8d7ee 100644 --- a/dashi/src/lib/components/DashiPlotToolbar.tsx +++ b/dashi/src/lib/components/DashiPlotToolbar.tsx @@ -3,10 +3,12 @@ import type { TopLevelParameter } from "vega-lite/build/src/spec/toplevel"; import LoupeIcon from "@mui/icons-material/Loupe"; import ReplayIcon from "@mui/icons-material/Replay"; import HighlightAltIcon from "@mui/icons-material/HighlightAlt"; +import BarChartIcon from "@mui/icons-material/BarChart"; import { Tooltip } from "@mui/material"; import { type PropertyChangeHandler } from "@/lib"; import type { PlotState } from "@/lib/types/state/component"; import { store } from "@/lib/store"; +import type { Transform } from "vega-lite/build/src/transform"; export interface DashiPlotToolbarProps { style: CSSProperties; @@ -26,7 +28,12 @@ export function DashiPlotToolbar({ const handleMouseEnter = () => setShowTooltip(true); const handleMouseLeave = () => setShowTooltip(false); - const getLatestChart = () => { + enum ChartStatus { + ORIGINAL, + CURRENT, + } + + const getLatestChart = (chartStatus: ChartStatus) => { const { contributionStatesRecord } = store.getState(); const contribPoint = "panels"; const contributionState = @@ -34,10 +41,17 @@ export function DashiPlotToolbar({ const plot = contributionState?.componentState?.components?.find( (component) => component.type === "Plot", ) as PlotState; - return plot?.chart; + switch (chartStatus) { + case ChartStatus.ORIGINAL: { + return plot?.originalChart; + } + case ChartStatus.CURRENT: { + return plot?.chart; + } + } }; - let chart = getLatestChart(); + let chart = getLatestChart(ChartStatus.CURRENT); // TODO: Try to memoize the following // const chart = useMemo(() => { @@ -63,11 +77,15 @@ export function DashiPlotToolbar({ enum Toolbar { PanAndZoom, Brush, + MiniMapZoom, + } + + interface ToolbarSpec { + params: TopLevelParameter[]; + transform?: Transform[]; } - const getToolbarSpec = ( - toolbar: Toolbar, - ): { params: TopLevelParameter[] } => { + const getToolbarSpec = (toolbar: Toolbar): ToolbarSpec => { switch (toolbar) { case Toolbar.PanAndZoom: return { @@ -88,55 +106,95 @@ export function DashiPlotToolbar({ }, ], }; + case Toolbar.MiniMapZoom: + return { + params: [ + { + name: "brush", + select: { type: "interval", encodings: ["x"] }, + }, + ], + transform: [ + { + filter: { param: "brush" }, + }, + ], + }; } }; - const enablePanAndZoomMode = () => { + const enableMiniMap = () => { resetMode(); - chart = getLatestChart(); - console.log("chart in zoom::", chart); - const panAndZoom = getToolbarSpec(Toolbar.PanAndZoom); - + chart = getLatestChart(ChartStatus.CURRENT); + const minimapZoom = getToolbarSpec(Toolbar.MiniMapZoom); + if (!chart) { + return null; + } if (!chart?.params?.find((param) => param.name === "grid")) { - const updatedSpec = { - ...chart, - params: [...(chart?.params || []), ...panAndZoom.params], + const { $schema, config, data, datasets, ...rest } = chart; + const updatedChart = { + $schema, + config, + data, + datasets, + vconcat: [ + { + ...rest, + transform: minimapZoom.transform, + }, + { + ...rest, + height: 60, + params: minimapZoom.params, + }, + ], }; return onPropertyChange({ componentType: "Plot", componentId: "plot", propertyName: "chart", - propertyValue: updatedSpec, + propertyValue: updatedChart, }); } }; - const resetMode = () => { - const filteredParams = chart?.params?.filter( - (param) => !(param.name === "grid" || param.name === "brush"), - ); + const enablePanAndZoomMode = () => { + resetMode(); + chart = getLatestChart(ChartStatus.CURRENT); + + const panAndZoom = getToolbarSpec(Toolbar.PanAndZoom); - if (filteredParams !== chart?.params) { - const spec = { + if (!chart?.params?.find((param) => param.name === "grid")) { + const updatedChart = { ...chart, - params: filteredParams, + params: [...(chart?.params || []), ...panAndZoom.params], }; return onPropertyChange({ componentType: "Plot", componentId: "plot", propertyName: "chart", - propertyValue: spec, + propertyValue: updatedChart, }); } }; + const resetMode = () => { + chart = getLatestChart(ChartStatus.ORIGINAL); + return onPropertyChange({ + componentType: "Plot", + componentId: "plot", + propertyName: "chart", + propertyValue: chart, + }); + }; + const enableBrushMode = () => { resetMode(); - chart = getLatestChart(); + chart = getLatestChart(ChartStatus.CURRENT); const brush = getToolbarSpec(Toolbar.Brush); if (!chart?.params?.find((param) => param.name === "brush")) { - const updatedSpec = { + const updatedChart = { ...chart, params: [...(chart?.params || []), ...brush.params], }; @@ -144,7 +202,7 @@ export function DashiPlotToolbar({ componentType: "Plot", componentId: "plot", propertyName: "chart", - propertyValue: updatedSpec, + propertyValue: updatedChart, }); } }; @@ -170,6 +228,12 @@ export function DashiPlotToolbar({ onClick={enablePanAndZoomMode} > + + + Date: Mon, 28 Oct 2024 15:14:06 +0100 Subject: [PATCH 5/7] Add box-zoom option (currently only works for bar chart) - dashipy --- dashipy/dashipy/components/plot.py | 2 ++ dashipy/my_extension/my_panel_2.py | 39 +++++++++++++++--------------- 2 files changed, 22 insertions(+), 19 deletions(-) diff --git a/dashipy/dashipy/components/plot.py b/dashipy/dashipy/components/plot.py index dd7f9f48..19581736 100644 --- a/dashipy/dashipy/components/plot.py +++ b/dashipy/dashipy/components/plot.py @@ -14,6 +14,8 @@ def to_dict(self) -> dict[str, Any]: d = super().to_dict() if self.chart is not None: d.update(chart=self.chart.to_dict()) + d.update(originalChart=self.chart.to_dict()) else: d.update(chart=None) + d.update(originalChart=None) return d diff --git a/dashipy/my_extension/my_panel_2.py b/dashipy/my_extension/my_panel_2.py index 61562ad5..f1600d9b 100644 --- a/dashipy/my_extension/my_panel_2.py +++ b/dashipy/my_extension/my_panel_2.py @@ -1,7 +1,7 @@ import altair as alt -from dashipy import (Component, Input, Output) -from dashipy.components import (Plot, Box, Dropdown) +from dashipy import Component, Input, Output +from dashipy.components import Plot, Box, Dropdown from dashipy.demo.contribs import Panel from dashipy.demo.context import Context @@ -49,27 +49,28 @@ def render_panel(ctx: Context) -> Component: ) def make_figure(ctx: Context, selected_dataset: int = 0) -> alt.Chart: dataset = ctx.datasets[selected_dataset] - slider = alt.binding_range(min=0, max=100, step=1, name='Cutoff ') - selector = alt.param(name='SelectorName', value=50, bind=slider) + slider = alt.binding_range(min=0, max=100, step=1, name="Cutoff ") + selector = alt.param(name="SelectorName", value=50, bind=slider) # Almost same as the chart in Panel 1, but here we use the Shorthand # notation for setting x,y and the tooltip, although they both give the # same output. We also call interactive() on this chart object which allows # to zoom in and out as well as move the chart around. - chart = alt.Chart(dataset).mark_bar().encode( - x='a:N', - y='b:Q', - tooltip=['a:N','b:Q'], - color = alt.condition( - 'datum.b < SelectorName', - alt.value('green'), - alt.value('yellow') + chart = ( + alt.Chart(dataset) + .mark_point() + .encode( + x="a:N", + y="b:Q", + tooltip=["a:N", "b:Q"], + color=alt.condition( + "datum.b < SelectorName", alt.value("green"), alt.value("red") + ), ) - ).properties( - width=300, - height=300, - title="Vega charts using Shorthand syntax", - ).add_params( - selector + .properties( + width=300, + height=300, + title="Vega charts using Shorthand syntax", + ) + .add_params(selector) ) return chart - From 4ad367f50a03888ddf0ddd7d25b13f0b62524248 Mon Sep 17 00:00:00 2001 From: Yogesh Kumar Baljeet Singh Date: Mon, 28 Oct 2024 15:23:22 +0100 Subject: [PATCH 6/7] Fix box-zoom for points --- dashi/src/lib/components/DashiPlotToolbar.tsx | 2 ++ dashipy/my_extension/my_panel_2.py | 9 +-------- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/dashi/src/lib/components/DashiPlotToolbar.tsx b/dashi/src/lib/components/DashiPlotToolbar.tsx index 2ce8d7ee..5e5f4f55 100644 --- a/dashi/src/lib/components/DashiPlotToolbar.tsx +++ b/dashi/src/lib/components/DashiPlotToolbar.tsx @@ -141,6 +141,7 @@ export function DashiPlotToolbar({ { ...rest, transform: minimapZoom.transform, + params: [], }, { ...rest, @@ -149,6 +150,7 @@ export function DashiPlotToolbar({ }, ], }; + console.log("after::", updatedChart); return onPropertyChange({ componentType: "Plot", componentId: "plot", diff --git a/dashipy/my_extension/my_panel_2.py b/dashipy/my_extension/my_panel_2.py index f1600d9b..46763a9a 100644 --- a/dashipy/my_extension/my_panel_2.py +++ b/dashipy/my_extension/my_panel_2.py @@ -49,12 +49,9 @@ def render_panel(ctx: Context) -> Component: ) def make_figure(ctx: Context, selected_dataset: int = 0) -> alt.Chart: dataset = ctx.datasets[selected_dataset] - slider = alt.binding_range(min=0, max=100, step=1, name="Cutoff ") - selector = alt.param(name="SelectorName", value=50, bind=slider) # Almost same as the chart in Panel 1, but here we use the Shorthand # notation for setting x,y and the tooltip, although they both give the - # same output. We also call interactive() on this chart object which allows - # to zoom in and out as well as move the chart around. + # same output. chart = ( alt.Chart(dataset) .mark_point() @@ -62,15 +59,11 @@ def make_figure(ctx: Context, selected_dataset: int = 0) -> alt.Chart: x="a:N", y="b:Q", tooltip=["a:N", "b:Q"], - color=alt.condition( - "datum.b < SelectorName", alt.value("green"), alt.value("red") - ), ) .properties( width=300, height=300, title="Vega charts using Shorthand syntax", ) - .add_params(selector) ) return chart From 52932641d00781b045ba8e6178a0f80ad5e027af Mon Sep 17 00:00:00 2001 From: Yogesh Kumar Baljeet Singh Date: Mon, 28 Oct 2024 15:58:01 +0100 Subject: [PATCH 7/7] Add tools to change marks --- dashi/src/lib/components/DashiPlotToolbar.tsx | 64 +++++++++++++++++-- 1 file changed, 57 insertions(+), 7 deletions(-) diff --git a/dashi/src/lib/components/DashiPlotToolbar.tsx b/dashi/src/lib/components/DashiPlotToolbar.tsx index 5e5f4f55..65ff634e 100644 --- a/dashi/src/lib/components/DashiPlotToolbar.tsx +++ b/dashi/src/lib/components/DashiPlotToolbar.tsx @@ -1,14 +1,19 @@ import React, { type CSSProperties, useState } from "react"; import type { TopLevelParameter } from "vega-lite/build/src/spec/toplevel"; +import { Tooltip } from "@mui/material"; +import { type PropertyChangeHandler } from "@/lib"; +import type { PlotState } from "@/lib/types/state/component"; +import type { Transform } from "vega-lite/build/src/transform"; import LoupeIcon from "@mui/icons-material/Loupe"; import ReplayIcon from "@mui/icons-material/Replay"; import HighlightAltIcon from "@mui/icons-material/HighlightAlt"; import BarChartIcon from "@mui/icons-material/BarChart"; -import { Tooltip } from "@mui/material"; -import { type PropertyChangeHandler } from "@/lib"; -import type { PlotState } from "@/lib/types/state/component"; +import ShowChartIcon from "@mui/icons-material/ShowChart"; +import ScatterPlotIcon from "@mui/icons-material/ScatterPlot"; +import LibraryAddIcon from "@mui/icons-material/LibraryAdd"; +import SignalCellular4BarIcon from "@mui/icons-material/SignalCellular4Bar"; + import { store } from "@/lib/store"; -import type { Transform } from "vega-lite/build/src/transform"; export interface DashiPlotToolbarProps { style: CSSProperties; @@ -209,6 +214,27 @@ export function DashiPlotToolbar({ } }; + enum MarkTypes { + POINT = "point", + BAR = "bar", + LINE = "line", + AREA = "area", + } + + const switchToMark = (markType: MarkTypes): void => { + chart = getLatestChart(ChartStatus.ORIGINAL); + const updatedChart = { + ...chart, + mark: { type: markType }, + }; + return onPropertyChange({ + componentType: "Plot", + componentId: "plot", + propertyName: "chart", + propertyValue: updatedChart, + }); + }; + return (
+ + switchToMark(MarkTypes.POINT)} + > + + + switchToMark(MarkTypes.LINE)} + > + + + switchToMark(MarkTypes.AREA)} + > + + + switchToMark(MarkTypes.BAR)} + > + - + >