Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 22 additions & 7 deletions examples/react-example/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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 {
Expand All @@ -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;
}
57 changes: 42 additions & 15 deletions examples/react-example/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand All @@ -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<psp.Table> {
Expand All @@ -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<psp.Table>;
config: pspViewer.ViewerConfigUpdate;
layout: perspective_workspace.PerspectiveWorkspaceConfig;
}

const App: React.FC = () => {
const [state, setState] = React.useState<ToolbarState>(() => ({
mounted: true,
table: createNewSuperstoreTable(),
layout: {
detail: { main: null },
sizes: [],
viewers: {},
},
config: { ...CONFIG },
}));

Expand All @@ -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);
};
Expand All @@ -122,14 +144,19 @@ const App: React.FC = () => {
</div>
{state.mounted && (
<>
<PerspectiveViewer table={state.table} />
<PerspectiveViewer client={state.table} />
<PerspectiveViewer
className="my-perspective-viewer"
table={state.table}
client={state.table}
config={state.config}
onClick={onClick}
onSelect={onSelect}
onConfigUpdate={onConfigUpdate}
onSelect={onSelect}
/>
<PerspectiveWorkspace
client={WORKER}
layout={state.layout}
onLayoutUpdate={onLayoutUpdate}
/>
</>
)}
Expand Down
1 change: 1 addition & 0 deletions packages/react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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:"
Expand Down
86 changes: 2 additions & 84 deletions packages/react/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<A>(
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<psp.Table>;
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<pspViewer.HTMLPerspectiveViewerElement | null>(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 (
<perspective-viewer
ref={setViewer}
id={props.id}
className={props.className}
hidden={props.hidden}
slot={props.slot}
style={props.style}
tabIndex={props.tabIndex}
title={props.title}
/>
);
}

/**
* A React wrapper component for `<perspective-viewer>` Custom Element.
*/
export const PerspectiveViewer: React.FC<PerspectiveViewerProps> = React.memo(
PerspectiveViewerImpl,
);
export * from "./viewer";
export * from "./workspace";
27 changes: 27 additions & 0 deletions packages/react/src/utils.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ 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";

export function usePspListener<A>(
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]);
}
86 changes: 86 additions & 0 deletions packages/react/src/viewer.tsx
Original file line number Diff line number Diff line change
@@ -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<pspViewer.HTMLPerspectiveViewerElement | null>(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 (
<perspective-viewer
ref={setViewer}
id={props.id}
className={props.className}
hidden={props.hidden}
slot={props.slot}
style={props.style}
tabIndex={props.tabIndex}
title={props.title}
/>
);
}

/**
* Props for the `<PerspectiveViewer>` component.
*/
export interface PerspectiveViewerProps {
client?: psp.Client | Promise<psp.Client> | psp.Table | Promise<psp.Table>;
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;
}

/**
* A React wrapper component for `<perspective-viewer>` Custom Element.
*/
export const PerspectiveViewer: React.FC<PerspectiveViewerProps> = React.memo(
PerspectiveViewerImpl,
);
Loading
Loading