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/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 c4af48b3..ba090ad8 100644 --- a/dashi/src/lib/components/DashiPlot.tsx +++ b/dashi/src/lib/components/DashiPlot.tsx @@ -1,22 +1,26 @@ 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 { DashiPlotToolbar } from "@/lib/components/DashiPlotToolbar"; export interface DashiPlotProps extends Omit { onPropertyChange: PropertyChangeHandler; + panelIndex: number; } export function DashiPlot({ id, style, chart, + panelIndex, onPropertyChange, }: DashiPlotProps) { if (!chart) { return
; } - const { datasets, ...spec } = chart; + const { datasets, ...specification } = chart; + const handleSignal = (_signalName: string, value: unknown) => { if (id) { return onPropertyChange({ @@ -28,12 +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..65ff634e --- /dev/null +++ b/dashi/src/lib/components/DashiPlotToolbar.tsx @@ -0,0 +1,303 @@ +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 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"; + +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); + + enum ChartStatus { + ORIGINAL, + CURRENT, + } + + const getLatestChart = (chartStatus: ChartStatus) => { + const { contributionStatesRecord } = store.getState(); + const contribPoint = "panels"; + const contributionState = + contributionStatesRecord[contribPoint][panelIndex]; + const plot = contributionState?.componentState?.components?.find( + (component) => component.type === "Plot", + ) as PlotState; + switch (chartStatus) { + case ChartStatus.ORIGINAL: { + return plot?.originalChart; + } + case ChartStatus.CURRENT: { + return plot?.chart; + } + } + }; + + let chart = getLatestChart(ChartStatus.CURRENT); + + // 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; + } + + enum Toolbar { + PanAndZoom, + Brush, + MiniMapZoom, + } + + interface ToolbarSpec { + params: TopLevelParameter[]; + transform?: Transform[]; + } + + const getToolbarSpec = (toolbar: Toolbar): ToolbarSpec => { + switch (toolbar) { + case Toolbar.PanAndZoom: + return { + params: [ + { + name: "grid", + select: "interval", + bind: "scales", + }, + ], + }; + case Toolbar.Brush: + return { + params: [ + { + name: "brush", + select: "interval", + }, + ], + }; + case Toolbar.MiniMapZoom: + return { + params: [ + { + name: "brush", + select: { type: "interval", encodings: ["x"] }, + }, + ], + transform: [ + { + filter: { param: "brush" }, + }, + ], + }; + } + }; + + const enableMiniMap = () => { + resetMode(); + chart = getLatestChart(ChartStatus.CURRENT); + const minimapZoom = getToolbarSpec(Toolbar.MiniMapZoom); + if (!chart) { + return null; + } + if (!chart?.params?.find((param) => param.name === "grid")) { + const { $schema, config, data, datasets, ...rest } = chart; + const updatedChart = { + $schema, + config, + data, + datasets, + vconcat: [ + { + ...rest, + transform: minimapZoom.transform, + params: [], + }, + { + ...rest, + height: 60, + params: minimapZoom.params, + }, + ], + }; + console.log("after::", updatedChart); + return onPropertyChange({ + componentType: "Plot", + componentId: "plot", + propertyName: "chart", + propertyValue: updatedChart, + }); + } + }; + + const enablePanAndZoomMode = () => { + resetMode(); + chart = getLatestChart(ChartStatus.CURRENT); + + const panAndZoom = getToolbarSpec(Toolbar.PanAndZoom); + + if (!chart?.params?.find((param) => param.name === "grid")) { + const updatedChart = { + ...chart, + params: [...(chart?.params || []), ...panAndZoom.params], + }; + return onPropertyChange({ + componentType: "Plot", + componentId: "plot", + propertyName: "chart", + propertyValue: updatedChart, + }); + } + }; + + const resetMode = () => { + chart = getLatestChart(ChartStatus.ORIGINAL); + return onPropertyChange({ + componentType: "Plot", + componentId: "plot", + propertyName: "chart", + propertyValue: chart, + }); + }; + + const enableBrushMode = () => { + resetMode(); + chart = getLatestChart(ChartStatus.CURRENT); + const brush = getToolbarSpec(Toolbar.Brush); + + if (!chart?.params?.find((param) => param.name === "brush")) { + const updatedChart = { + ...chart, + params: [...(chart?.params || []), ...brush.params], + }; + return onPropertyChange({ + componentType: "Plot", + componentId: "plot", + propertyName: "chart", + propertyValue: updatedChart, + }); + } + }; + + 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 ( +
+ {showTooltip && ( +
+ + switchToMark(MarkTypes.POINT)} + > + + + switchToMark(MarkTypes.LINE)} + > + + + switchToMark(MarkTypes.AREA)} + > + + + switchToMark(MarkTypes.BAR)} + > + + + + + + + + + + + + + +
+ )} + {children} +
+ ); +} diff --git a/dashi/src/lib/types/state/component.ts b/dashi/src/lib/types/state/component.ts index 8b4020ab..eb55dcbf 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,13 +38,17 @@ export interface CheckboxState extends ComponentState { export interface PlotState extends ComponentState { type: "Plot"; - chart: - | (VisualizationSpec & { - datasets?: Record; // Add the datasets property - }) - | null; + chart?: Specification; + originalChart?: Specification; } +export type Specification = + | (VisualizationSpec & { + datasets?: Record; + params?: TopLevelParameter[]; + }) + | null; + export interface BoxState extends ContainerState { type: "Box"; } 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_1.py b/dashipy/my_extension/my_panel_1.py index 552855be..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(cornerRadius=corner_var).encode( + chart = alt.Chart(dataset).mark_bar().encode( x=alt.X('a:N', title='a'), y=alt.Y('b:Q', title='b'), tooltip=[ @@ -71,7 +67,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..46763a9a 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,21 @@ 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. - 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') + # same output. + chart = ( + alt.Chart(dataset) + .mark_point() + .encode( + x="a:N", + y="b:Q", + tooltip=["a:N", "b:Q"], ) - ).properties( - width=300, - height=300, - title="Vega charts using Shorthand syntax" - ).add_params( - selector - ).interactive() + .properties( + width=300, + height=300, + title="Vega charts using Shorthand syntax", + ) + ) return chart -