Skip to content

Commit 6a0f895

Browse files
sinistersnaretexodus
authored andcommitted
React bindings for <perspective-workspace>
Signed-off-by: Andrew Stein <steinlink@gmail.com>
1 parent 390b7ea commit 6a0f895

File tree

12 files changed

+534
-115
lines changed

12 files changed

+534
-115
lines changed

examples/react-example/src/index.css

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,9 @@
1414
body {
1515
margin: 0;
1616
background-color: #f0f0f0;
17-
font-family: "ui-monospace", "SFMono-Regular", "SF Mono", "Menlo",
18-
"Consolas", "Liberation Mono", monospace;
17+
font-family:
18+
"ui-monospace", "SFMono-Regular", "SF Mono", "Menlo", "Consolas",
19+
"Liberation Mono", monospace;
1920
}
2021

2122
.container {
@@ -26,13 +27,26 @@ body {
2627
bottom: 0;
2728

2829
display: grid;
29-
gap: 8px;
3030
grid-template-columns: 1fr 1fr;
31-
grid-template-rows: 45px 1fr;
31+
grid-template-rows: 45px 1fr 1fr;
3232
}
3333

3434
perspective-viewer {
35-
grid-row: 2;
35+
/* grid-row-start: 2;
36+
grid-row-end: 3; */
37+
grid-column: 1;
38+
margin: 12px;
39+
}
40+
41+
perspective-workspace {
42+
grid-row-start: 2;
43+
grid-row-end: 4;
44+
grid-column: 2;
45+
overflow: hidden;
46+
}
47+
48+
perspective-workspace perspective-viewer {
49+
margin: 0px;
3650
}
3751

3852
.toolbar {
@@ -49,6 +63,7 @@ perspective-viewer {
4963
}
5064

5165
button {
52-
font-family: "ui-monospace", "SFMono-Regular", "SF Mono", "Menlo",
53-
"Consolas", "Liberation Mono", monospace;
66+
font-family:
67+
"ui-monospace", "SFMono-Regular", "SF Mono", "Menlo", "Consolas",
68+
"Liberation Mono", monospace;
5469
}

examples/react-example/src/index.tsx

Lines changed: 42 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,27 @@
2020

2121
import perspective from "@perspective-dev/client";
2222
import perspective_viewer from "@perspective-dev/viewer";
23+
import perspective_workspace from "@perspective-dev/workspace";
24+
import "@perspective-dev/workspace";
2325
import "@perspective-dev/viewer-datagrid";
2426
import "@perspective-dev/viewer-d3fc";
2527

28+
import * as React from "react";
29+
import { createRoot } from "react-dom/client";
30+
import {
31+
PerspectiveViewer,
32+
PerspectiveWorkspace,
33+
} from "@perspective-dev/react";
34+
35+
import "@perspective-dev/viewer/dist/css/themes.css";
36+
import "@perspective-dev/workspace/dist/css/pro.css";
37+
import "./index.css";
38+
39+
import type * as psp from "@perspective-dev/client";
40+
import type * as pspViewer from "@perspective-dev/viewer";
41+
42+
import SUPERSTORE_ARROW from "superstore-arrow/superstore.lz4.arrow";
43+
2644
import SERVER_WASM from "@perspective-dev/server/dist/wasm/perspective-server.wasm";
2745
import CLIENT_WASM from "@perspective-dev/viewer/dist/wasm/perspective-viewer.wasm";
2846

@@ -37,11 +55,6 @@ await Promise.all([
3755
// table creation function which both downloads data and loads it into the
3856
// engine.
3957

40-
import type * as psp from "@perspective-dev/client";
41-
import type * as pspViewer from "@perspective-dev/viewer";
42-
43-
import SUPERSTORE_ARROW from "superstore-arrow/superstore.lz4.arrow";
44-
4558
const WORKER = await perspective.worker();
4659

4760
async function createNewSuperstoreTable(): Promise<psp.Table> {
@@ -60,23 +73,22 @@ const CONFIG: pspViewer.ViewerConfigUpdate = {
6073

6174
// The React application itself
6275

63-
import * as React from "react";
64-
import { createRoot } from "react-dom/client";
65-
import { PerspectiveViewer } from "@perspective-dev/react";
66-
67-
import "@perspective-dev/viewer/dist/css/themes.css";
68-
import "./index.css";
69-
7076
interface ToolbarState {
7177
mounted: boolean;
7278
table?: Promise<psp.Table>;
7379
config: pspViewer.ViewerConfigUpdate;
80+
layout: perspective_workspace.PerspectiveWorkspaceConfig;
7481
}
7582

7683
const App: React.FC = () => {
7784
const [state, setState] = React.useState<ToolbarState>(() => ({
7885
mounted: true,
7986
table: createNewSuperstoreTable(),
87+
layout: {
88+
detail: { main: null },
89+
sizes: [],
90+
viewers: {},
91+
},
8092
config: { ...CONFIG },
8193
}));
8294

@@ -102,9 +114,19 @@ const App: React.FC = () => {
102114

103115
const onConfigUpdate = (config: pspViewer.ViewerConfigUpdate) => {
104116
console.log("Config Update Event", config);
117+
delete config.table;
105118
setState({ ...state, config });
106119
};
107120

121+
const onLayoutUpdate = (
122+
layout: perspective_workspace.PerspectiveWorkspaceConfig,
123+
) => {
124+
console.log("Layout Update Event", layout);
125+
126+
// delete config.table;
127+
setState({ ...state, layout });
128+
};
129+
108130
const onClick = (detail: pspViewer.PerspectiveClickEventDetail) => {
109131
console.log("Click Event,", detail);
110132
};
@@ -122,14 +144,19 @@ const App: React.FC = () => {
122144
</div>
123145
{state.mounted && (
124146
<>
125-
<PerspectiveViewer table={state.table} />
147+
<PerspectiveViewer client={state.table} />
126148
<PerspectiveViewer
127149
className="my-perspective-viewer"
128-
table={state.table}
150+
client={state.table}
129151
config={state.config}
130152
onClick={onClick}
131-
onSelect={onSelect}
132153
onConfigUpdate={onConfigUpdate}
154+
onSelect={onSelect}
155+
/>
156+
<PerspectiveWorkspace
157+
client={WORKER}
158+
layout={state.layout}
159+
onLayoutUpdate={onLayoutUpdate}
133160
/>
134161
</>
135162
)}

packages/react/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
"dependencies": {
2222
"@perspective-dev/client": "workspace:",
2323
"@perspective-dev/viewer": "workspace:",
24+
"@perspective-dev/workspace": "workspace:",
2425
"@types/react": "catalog:",
2526
"react": "catalog:",
2627
"react-dom": "catalog:"

packages/react/src/index.tsx

Lines changed: 2 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -20,87 +20,5 @@
2020
* @module
2121
*/
2222

23-
import * as React from "react";
24-
import type * as psp from "@perspective-dev/client";
25-
import type * as pspViewer from "@perspective-dev/viewer";
26-
27-
function usePspListener<A>(
28-
viewer: HTMLElement | null,
29-
name: string,
30-
f?: (x: A) => void,
31-
) {
32-
React.useEffect(() => {
33-
if (!f) return;
34-
const ctx = new AbortController();
35-
const callback = (e: Event) => f((e as CustomEvent).detail);
36-
viewer?.addEventListener(name, callback, { signal: ctx.signal });
37-
return () => ctx.abort();
38-
}, [viewer, f]);
39-
}
40-
41-
export interface PerspectiveViewerProps {
42-
table?: psp.Table | Promise<psp.Table>;
43-
config?: pspViewer.ViewerConfigUpdate;
44-
onConfigUpdate?: (config: pspViewer.ViewerConfigUpdate) => void;
45-
onClick?: (data: pspViewer.PerspectiveClickEventDetail) => void;
46-
onSelect?: (data: pspViewer.PerspectiveSelectEventDetail) => void;
47-
48-
// Applicable props from `React.HTMLAttributes`, which we cannot extend
49-
// directly because Perspective changes the signature of `onClick`.
50-
className?: string | undefined;
51-
hidden?: boolean | undefined;
52-
id?: string | undefined;
53-
slot?: string | undefined;
54-
style?: React.CSSProperties | undefined;
55-
tabIndex?: number | undefined;
56-
title?: string | undefined;
57-
}
58-
59-
function PerspectiveViewerImpl(props: PerspectiveViewerProps) {
60-
const [viewer, setViewer] =
61-
React.useState<pspViewer.HTMLPerspectiveViewerElement | null>(null);
62-
63-
React.useEffect(() => {
64-
return () => {
65-
viewer?.delete();
66-
};
67-
}, [viewer]);
68-
69-
React.useEffect(() => {
70-
if (props.table) {
71-
viewer?.load(props.table);
72-
} else {
73-
viewer?.eject();
74-
}
75-
}, [viewer, props.table]);
76-
77-
React.useEffect(() => {
78-
if (props.table && props.config) {
79-
viewer?.restore(props.config);
80-
}
81-
}, [viewer, props.table, JSON.stringify(props.config)]);
82-
83-
usePspListener(viewer, "perspective-click", props.onClick);
84-
usePspListener(viewer, "perspective-select", props.onSelect);
85-
usePspListener(viewer, "perspective-config-update", props.onConfigUpdate);
86-
87-
return (
88-
<perspective-viewer
89-
ref={setViewer}
90-
id={props.id}
91-
className={props.className}
92-
hidden={props.hidden}
93-
slot={props.slot}
94-
style={props.style}
95-
tabIndex={props.tabIndex}
96-
title={props.title}
97-
/>
98-
);
99-
}
100-
101-
/**
102-
* A React wrapper component for `<perspective-viewer>` Custom Element.
103-
*/
104-
export const PerspectiveViewer: React.FC<PerspectiveViewerProps> = React.memo(
105-
PerspectiveViewerImpl,
106-
);
23+
export * from "./viewer";
24+
export * from "./workspace";

packages/react/src/utils.tsx

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
2+
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
3+
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
4+
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
5+
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
6+
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
7+
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
8+
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
9+
// ┃ This file is part of the Perspective library, distributed under the terms ┃
10+
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
11+
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
12+
13+
import * as React from "react";
14+
15+
export function usePspListener<A>(
16+
el: HTMLElement | undefined | null,
17+
event: string,
18+
cb?: (x: A) => void,
19+
) {
20+
React.useEffect(() => {
21+
if (!cb || !el) return;
22+
const ctx = new AbortController();
23+
const callback = (e: Event) => cb((e as CustomEvent).detail);
24+
el?.addEventListener(event, callback, { signal: ctx.signal });
25+
return () => ctx.abort();
26+
}, [el, cb]);
27+
}

packages/react/src/viewer.tsx

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
2+
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
3+
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
4+
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
5+
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
6+
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
7+
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
8+
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
9+
// ┃ This file is part of the Perspective library, distributed under the terms ┃
10+
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
11+
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
12+
13+
import * as React from "react";
14+
import type * as psp from "@perspective-dev/client";
15+
import type * as pspViewer from "@perspective-dev/viewer";
16+
import { usePspListener } from "./utils";
17+
18+
function PerspectiveViewerImpl(props: PerspectiveViewerProps) {
19+
const [viewer, setViewer] =
20+
React.useState<pspViewer.HTMLPerspectiveViewerElement | null>(null);
21+
22+
React.useEffect(() => {
23+
return () => {
24+
viewer?.delete();
25+
};
26+
}, [viewer]);
27+
28+
React.useEffect(() => {
29+
if (props.client) {
30+
viewer?.load(props.client);
31+
} else {
32+
viewer?.eject();
33+
}
34+
}, [viewer, props.client]);
35+
36+
React.useEffect(() => {
37+
if (props.client && props.config) {
38+
viewer?.restore(props.config);
39+
}
40+
}, [viewer, props.client, JSON.stringify(props.config)]);
41+
42+
usePspListener(viewer, "perspective-click", props.onClick);
43+
usePspListener(viewer, "perspective-select", props.onSelect);
44+
usePspListener(viewer, "perspective-config-update", props.onConfigUpdate);
45+
46+
return (
47+
<perspective-viewer
48+
ref={setViewer}
49+
id={props.id}
50+
className={props.className}
51+
hidden={props.hidden}
52+
slot={props.slot}
53+
style={props.style}
54+
tabIndex={props.tabIndex}
55+
title={props.title}
56+
/>
57+
);
58+
}
59+
60+
/**
61+
* Props for the `<PerspectiveViewer>` component.
62+
*/
63+
export interface PerspectiveViewerProps {
64+
client?: psp.Client | Promise<psp.Client> | psp.Table | Promise<psp.Table>;
65+
config?: pspViewer.ViewerConfigUpdate;
66+
onConfigUpdate?: (config: pspViewer.ViewerConfigUpdate) => void;
67+
onClick?: (data: pspViewer.PerspectiveClickEventDetail) => void;
68+
onSelect?: (data: pspViewer.PerspectiveSelectEventDetail) => void;
69+
70+
// Applicable props from `React.HTMLAttributes`, which we cannot extend
71+
// directly because Perspective changes the signature of `onClick`.
72+
className?: string | undefined;
73+
hidden?: boolean | undefined;
74+
id?: string | undefined;
75+
slot?: string | undefined;
76+
style?: React.CSSProperties | undefined;
77+
tabIndex?: number | undefined;
78+
title?: string | undefined;
79+
}
80+
81+
/**
82+
* A React wrapper component for `<perspective-viewer>` Custom Element.
83+
*/
84+
export const PerspectiveViewer: React.FC<PerspectiveViewerProps> = React.memo(
85+
PerspectiveViewerImpl,
86+
);

0 commit comments

Comments
 (0)