diff --git a/examples/react-example/src/index.css b/examples/react-example/src/index.css index b9ae4513e0..6a2929882c 100644 --- a/examples/react-example/src/index.css +++ b/examples/react-example/src/index.css @@ -14,8 +14,9 @@ body { margin: 0; background-color: #f0f0f0; - font-family: "ui-monospace", "SFMono-Regular", "SF Mono", "Menlo", - "Consolas", "Liberation Mono", monospace; + font-family: + "ui-monospace", "SFMono-Regular", "SF Mono", "Menlo", "Consolas", + "Liberation Mono", monospace; } .container { @@ -26,13 +27,26 @@ body { bottom: 0; display: grid; - gap: 8px; grid-template-columns: 1fr 1fr; - grid-template-rows: 45px 1fr; + grid-template-rows: 45px 1fr 1fr; } perspective-viewer { - grid-row: 2; + /* grid-row-start: 2; + grid-row-end: 3; */ + grid-column: 1; + margin: 12px; +} + +perspective-workspace { + grid-row-start: 2; + grid-row-end: 4; + grid-column: 2; + overflow: hidden; +} + +perspective-workspace perspective-viewer { + margin: 0px; } .toolbar { @@ -49,6 +63,7 @@ perspective-viewer { } button { - font-family: "ui-monospace", "SFMono-Regular", "SF Mono", "Menlo", - "Consolas", "Liberation Mono", monospace; + font-family: + "ui-monospace", "SFMono-Regular", "SF Mono", "Menlo", "Consolas", + "Liberation Mono", monospace; } diff --git a/examples/react-example/src/index.tsx b/examples/react-example/src/index.tsx index 23a0ea6429..3ba740324d 100644 --- a/examples/react-example/src/index.tsx +++ b/examples/react-example/src/index.tsx @@ -20,9 +20,27 @@ import perspective from "@perspective-dev/client"; import perspective_viewer from "@perspective-dev/viewer"; +import perspective_workspace from "@perspective-dev/workspace"; +import "@perspective-dev/workspace"; import "@perspective-dev/viewer-datagrid"; import "@perspective-dev/viewer-d3fc"; +import * as React from "react"; +import { createRoot } from "react-dom/client"; +import { + PerspectiveViewer, + PerspectiveWorkspace, +} from "@perspective-dev/react"; + +import "@perspective-dev/viewer/dist/css/themes.css"; +import "@perspective-dev/workspace/dist/css/pro.css"; +import "./index.css"; + +import type * as psp from "@perspective-dev/client"; +import type * as pspViewer from "@perspective-dev/viewer"; + +import SUPERSTORE_ARROW from "superstore-arrow/superstore.lz4.arrow"; + import SERVER_WASM from "@perspective-dev/server/dist/wasm/perspective-server.wasm"; import CLIENT_WASM from "@perspective-dev/viewer/dist/wasm/perspective-viewer.wasm"; @@ -37,11 +55,6 @@ await Promise.all([ // table creation function which both downloads data and loads it into the // engine. -import type * as psp from "@perspective-dev/client"; -import type * as pspViewer from "@perspective-dev/viewer"; - -import SUPERSTORE_ARROW from "superstore-arrow/superstore.lz4.arrow"; - const WORKER = await perspective.worker(); async function createNewSuperstoreTable(): Promise { @@ -60,23 +73,22 @@ const CONFIG: pspViewer.ViewerConfigUpdate = { // The React application itself -import * as React from "react"; -import { createRoot } from "react-dom/client"; -import { PerspectiveViewer } from "@perspective-dev/react"; - -import "@perspective-dev/viewer/dist/css/themes.css"; -import "./index.css"; - interface ToolbarState { mounted: boolean; table?: Promise; config: pspViewer.ViewerConfigUpdate; + layout: perspective_workspace.PerspectiveWorkspaceConfig; } const App: React.FC = () => { const [state, setState] = React.useState(() => ({ mounted: true, table: createNewSuperstoreTable(), + layout: { + detail: { main: null }, + sizes: [], + viewers: {}, + }, config: { ...CONFIG }, })); @@ -102,9 +114,19 @@ const App: React.FC = () => { const onConfigUpdate = (config: pspViewer.ViewerConfigUpdate) => { console.log("Config Update Event", config); + delete config.table; setState({ ...state, config }); }; + const onLayoutUpdate = ( + layout: perspective_workspace.PerspectiveWorkspaceConfig, + ) => { + console.log("Layout Update Event", layout); + + // delete config.table; + setState({ ...state, layout }); + }; + const onClick = (detail: pspViewer.PerspectiveClickEventDetail) => { console.log("Click Event,", detail); }; @@ -122,14 +144,19 @@ const App: React.FC = () => { {state.mounted && ( <> - + + )} diff --git a/packages/react/package.json b/packages/react/package.json index c602bee4b1..89dba0b0e2 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -21,6 +21,7 @@ "dependencies": { "@perspective-dev/client": "workspace:", "@perspective-dev/viewer": "workspace:", + "@perspective-dev/workspace": "workspace:", "@types/react": "catalog:", "react": "catalog:", "react-dom": "catalog:" diff --git a/packages/react/src/index.tsx b/packages/react/src/index.tsx index 64d775603d..379c7f35a8 100644 --- a/packages/react/src/index.tsx +++ b/packages/react/src/index.tsx @@ -20,87 +20,5 @@ * @module */ -import * as React from "react"; -import type * as psp from "@perspective-dev/client"; -import type * as pspViewer from "@perspective-dev/viewer"; - -function usePspListener( - viewer: HTMLElement | null, - name: string, - f?: (x: A) => void, -) { - React.useEffect(() => { - if (!f) return; - const ctx = new AbortController(); - const callback = (e: Event) => f((e as CustomEvent).detail); - viewer?.addEventListener(name, callback, { signal: ctx.signal }); - return () => ctx.abort(); - }, [viewer, f]); -} - -export interface PerspectiveViewerProps { - table?: psp.Table | Promise; - config?: pspViewer.ViewerConfigUpdate; - onConfigUpdate?: (config: pspViewer.ViewerConfigUpdate) => void; - onClick?: (data: pspViewer.PerspectiveClickEventDetail) => void; - onSelect?: (data: pspViewer.PerspectiveSelectEventDetail) => void; - - // Applicable props from `React.HTMLAttributes`, which we cannot extend - // directly because Perspective changes the signature of `onClick`. - className?: string | undefined; - hidden?: boolean | undefined; - id?: string | undefined; - slot?: string | undefined; - style?: React.CSSProperties | undefined; - tabIndex?: number | undefined; - title?: string | undefined; -} - -function PerspectiveViewerImpl(props: PerspectiveViewerProps) { - const [viewer, setViewer] = - React.useState(null); - - React.useEffect(() => { - return () => { - viewer?.delete(); - }; - }, [viewer]); - - React.useEffect(() => { - if (props.table) { - viewer?.load(props.table); - } else { - viewer?.eject(); - } - }, [viewer, props.table]); - - React.useEffect(() => { - if (props.table && props.config) { - viewer?.restore(props.config); - } - }, [viewer, props.table, JSON.stringify(props.config)]); - - usePspListener(viewer, "perspective-click", props.onClick); - usePspListener(viewer, "perspective-select", props.onSelect); - usePspListener(viewer, "perspective-config-update", props.onConfigUpdate); - - return ( - ( + el: HTMLElement | undefined | null, + event: string, + cb?: (x: A) => void, +) { + React.useEffect(() => { + if (!cb || !el) return; + const ctx = new AbortController(); + const callback = (e: Event) => cb((e as CustomEvent).detail); + el?.addEventListener(event, callback, { signal: ctx.signal }); + return () => ctx.abort(); + }, [el, cb]); +} diff --git a/packages/react/src/viewer.tsx b/packages/react/src/viewer.tsx new file mode 100644 index 0000000000..dc9631555a --- /dev/null +++ b/packages/react/src/viewer.tsx @@ -0,0 +1,86 @@ +// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃ +// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃ +// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃ +// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃ +// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ +// ┃ Copyright (c) 2017, the Perspective Authors. ┃ +// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃ +// ┃ This file is part of the Perspective library, distributed under the terms ┃ +// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ +// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + +import * as React from "react"; +import type * as psp from "@perspective-dev/client"; +import type * as pspViewer from "@perspective-dev/viewer"; +import { usePspListener } from "./utils"; + +function PerspectiveViewerImpl(props: PerspectiveViewerProps) { + const [viewer, setViewer] = + React.useState(null); + + React.useEffect(() => { + return () => { + viewer?.delete(); + }; + }, [viewer]); + + React.useEffect(() => { + if (props.client) { + viewer?.load(props.client); + } else { + viewer?.eject(); + } + }, [viewer, props.client]); + + React.useEffect(() => { + if (props.client && props.config) { + viewer?.restore(props.config); + } + }, [viewer, props.client, JSON.stringify(props.config)]); + + usePspListener(viewer, "perspective-click", props.onClick); + usePspListener(viewer, "perspective-select", props.onSelect); + usePspListener(viewer, "perspective-config-update", props.onConfigUpdate); + + return ( +