From 96115a9a701aa7801c908ca7d238146f48629e8b Mon Sep 17 00:00:00 2001 From: Henning Dieterichs Date: Fri, 21 Apr 2023 11:52:30 +0200 Subject: [PATCH] Adds support for external visualizer functions. --- playground/src/visualizations.ts | 54 ++++----- .../VisJsGraphViewer.tsx | 92 ++++++++++++++- .../visualizers/plotly-visualizer/index.tsx | 65 ++++++----- visualization-core/package.json | 2 +- .../src/CanvasVisualization.tsx | 30 +++++ visualization-core/src/ReactVisualization.tsx | 107 +++++++----------- .../src/RegisterVisualizerFn.ts | 26 +++++ ...alizationData.tsx => VisualizationData.ts} | 0 visualization-core/src/VisualizationView.tsx | 14 +-- .../src/{Visualizer.tsx => Visualizer.ts} | 58 ++++++---- visualization-core/src/VisualizerRegistry.ts | 16 ++- ...eateVisualizer.tsx => createVisualizer.ts} | 12 +- visualization-core/src/index.tsx | 1 + 13 files changed, 319 insertions(+), 158 deletions(-) create mode 100644 visualization-core/src/CanvasVisualization.tsx create mode 100644 visualization-core/src/RegisterVisualizerFn.ts rename visualization-core/src/{VisualizationData.tsx => VisualizationData.ts} (100%) rename visualization-core/src/{Visualizer.tsx => Visualizer.ts} (62%) rename visualization-core/src/{createVisualizer.tsx => createVisualizer.ts} (75%) diff --git a/playground/src/visualizations.ts b/playground/src/visualizations.ts index 2300312..79b816f 100644 --- a/playground/src/visualizations.ts +++ b/playground/src/visualizations.ts @@ -1,27 +1,27 @@ -//// Uncomment the visualization that you work on to increase webpack build speed! -//// If you create a new visualization, add it here! - -// import "@hediet/visualization-bundle/dist/visualizers/ast-visualizer"; /* -// import "@hediet/visualization-bundle/dist/visualizers/graph/dot-graphviz-visualizer" /* -// import "@hediet/visualization-bundle/dist/visualizers/graph/graph-graphviz-visualizer" /* -// import "@hediet/visualization-bundle/dist/visualizers/graph/graph-visjs-visualizer" /* -// import "@hediet/visualization-bundle/dist/visualizers/grid-visualizer" /* -// import "@hediet/visualization-bundle/dist/visualizers/image-visualizer"; /* -// import "@hediet/visualization-bundle/dist/visualizers/monaco-text-visualizer" /* -// import "@hediet/visualization-bundle/dist/visualizers/monaco-text-diff-visualizer"; /* -// import "@hediet/visualization-bundle/dist/visualizers/perspective-table-visualizer" /* -// import "@hediet/visualization-bundle/dist/visualizers/plotly-visualizer" /* -// import "@hediet/visualization-bundle/dist/visualizers/simple-text-visualizer"; /* -// import "@hediet/visualization-bundle/dist/visualizers/source-visualizer"; /* -// import "@hediet/visualization-bundle/dist/visualizers/svg-visualizer" /* -// import "@hediet/visualization-bundle/dist/visualizers/tree-visualizer"; /* - -// This bundles the monaco editor. Uncomment it to load monaco dynamically. -// Dynamic loading increases webpack build speed significantly. -if (typeof process === undefined) { - // We check for process so that we don't load monaco from nodejs. - require("monaco-editor"); -} - -// This import bundles *all* visualizations. This makes webpack super slow. -import "@hediet/visualization-bundle"; // */ +//// Uncomment the visualization that you work on to increase webpack build speed! +//// If you create a new visualization, add it here! + +// import "@hediet/visualization-bundle/dist/visualizers/ast-visualizer"; /* +// import "@hediet/visualization-bundle/dist/visualizers/graph/dot-graphviz-visualizer" /* +// import "@hediet/visualization-bundle/dist/visualizers/graph/graph-graphviz-visualizer" /* +// import "@hediet/visualization-bundle/dist/visualizers/graph/graph-visjs-visualizer" /* +// import "@hediet/visualization-bundle/dist/visualizers/grid-visualizer" /* +// import "@hediet/visualization-bundle/dist/visualizers/image-visualizer"; /* +// import "@hediet/visualization-bundle/dist/visualizers/monaco-text-visualizer" /* +// import "@hediet/visualization-bundle/dist/visualizers/monaco-text-diff-visualizer"; /* +// import "@hediet/visualization-bundle/dist/visualizers/perspective-table-visualizer" /* +// import "@hediet/visualization-bundle/dist/visualizers/plotly-visualizer" /* +// import "@hediet/visualization-bundle/dist/visualizers/simple-text-visualizer"; /* +// import "@hediet/visualization-bundle/dist/visualizers/source-visualizer"; /* +// import "@hediet/visualization-bundle/dist/visualizers/svg-visualizer" /* +// import "@hediet/visualization-bundle/dist/visualizers/tree-visualizer"; /* + +// This bundles the monaco editor. Uncomment it to load monaco dynamically. +// Dynamic loading increases webpack build speed significantly. +if (typeof process === undefined) { + // We check for process so that we don't load monaco from nodejs. + require("monaco-editor"); +} + +// This import bundles *all* visualizations. This makes webpack super slow. +import "@hediet/visualization-bundle"; // */ diff --git a/visualization-bundle/src/visualizers/graph/graph-visjs-visualizer/VisJsGraphViewer.tsx b/visualization-bundle/src/visualizers/graph/graph-visjs-visualizer/VisJsGraphViewer.tsx index 0cf8781..d786dea 100644 --- a/visualization-bundle/src/visualizers/graph/graph-visjs-visualizer/VisJsGraphViewer.tsx +++ b/visualization-bundle/src/visualizers/graph/graph-visjs-visualizer/VisJsGraphViewer.tsx @@ -46,7 +46,7 @@ export class VisJsGraphViewer extends React.Component<{ shape: n.shape, }); } - this.nodes.forEach(item => { + this.nodes.forEach((item) => { if (!newNodes.has(item.id)) { this.nodes.remove(item); } @@ -74,7 +74,7 @@ export class VisJsGraphViewer extends React.Component<{ ], }); } - this.edges.forEach(item => { + this.edges.forEach((item) => { if (!newEdges.has(item.id)) { this.edges.remove(item); } @@ -99,6 +99,92 @@ export class VisJsGraphViewer extends React.Component<{ }, }, }; - const network = new Network(this.divRef.current!, data, options); + this.network = new Network(this.divRef.current!, data, options); + this.divRef.current!.setAttribute("tabindex", "0"); + document.addEventListener("copy", this.onCopy); + } + + private network: Network | undefined; + private readonly onCopy = (e: ClipboardEvent) => { + if (!this.network) { + return; + } + if ( + !( + document.activeElement && + document.activeElement.className === "vis-network" + ) + ) { + return; + } + + const n = this.network as any; + + const nodesText: string[] = []; + + let id = 10; + const visJsIdToNodeId = new Map(); + + // export to draw.io + for (const node of Object.values(n.body.nodes) as any) { + const label = node.shape.options.label; + if (label === undefined) { + continue; + } + + let style = ""; + if (node.shape.constructor.name === "Ellipse") { + style += "ellipse;"; + } else { + style += "rounded=1;"; + } + + const nodeId = id++; + visJsIdToNodeId.set(node.id, nodeId); + + nodesText.push( + ` + + +` + ); + } + + for (const edge of Object.values(n.body.edges) as any) { + const label = edge.options.label || ""; + const edgeId = id++; + + nodesText.push( + ` + + +` + ); + } + + const data = ` + + + + + + ${nodesText.join("\n")} + +`; + + /* + + + + */ + + e.clipboardData!.setData("text/plain", encodeURIComponent(data)); + e.preventDefault(); + }; + + componentWillUnmount() { + document.removeEventListener("copy", this.onCopy); } } diff --git a/visualization-bundle/src/visualizers/plotly-visualizer/index.tsx b/visualization-bundle/src/visualizers/plotly-visualizer/index.tsx index 4b0f3b2..f18ea7f 100644 --- a/visualization-bundle/src/visualizers/plotly-visualizer/index.tsx +++ b/visualization-bundle/src/visualizers/plotly-visualizer/index.tsx @@ -1,24 +1,23 @@ -import * as React from "react"; import { - sOpenObject, - sLiteral, sArrayOf, + sLiteral, + sNull, + sNumber, + sOpenObject, sOptionalProp, - sAny, sProp, sString, sUnion, - sNumber, - sNull, } from "@hediet/semantic-json"; -import { makeLazyLoadable } from "../../utils/LazyLoadable"; +import { Deferred } from "@hediet/std/synchronization"; import { + createLazyReactVisualization, createVisualizer, globalVisualizationFactory, - createReactVisualization, - createLazyReactVisualization, } from "@hediet/visualization-core"; +import * as React from "react"; import { visualizationNs } from "../../consts"; +import { makeLazyLoadable } from "../../utils/LazyLoadable"; const PlotlyViewerLazyLoadable = makeLazyLoadable( async () => (await import("./PlotlyViewer")).PlotlyViewer @@ -45,16 +44,22 @@ export const plotlyVisualizer = createVisualizer({ x: sOptionalProp(sDatumArr), y: sOptionalProp(sDatumArr), z: sOptionalProp(sDatumArr), - cells: sOptionalProp(sOpenObject({ - values: sArrayOf(sDatumArr), - })), - header: sOptionalProp(sOpenObject({ - values: sDatumArr, - })), - domain: sOptionalProp(sOpenObject({ - x: sArrayOf(sNumber()), - y: sArrayOf(sNumber()), - })), + cells: sOptionalProp( + sOpenObject({ + values: sArrayOf(sDatumArr), + }) + ), + header: sOptionalProp( + sOpenObject({ + values: sDatumArr, + }) + ), + domain: sOptionalProp( + sOpenObject({ + x: sArrayOf(sNumber()), + y: sArrayOf(sNumber()), + }) + ), type: sOptionalProp( sUnion([ sLiteral("bar"), @@ -129,14 +134,20 @@ export const plotlyVisualizer = createVisualizer({ priority: 1000, preload: PlotlyViewerLazyLoadable.preload, }, - ({ theme, readyCallback }) => ( - - ) + ({ theme }) => { + const b = new Deferred(); + return { + node: ( + b.resolve} + /> + ), + ready: b.promise, + }; + } ), }); diff --git a/visualization-core/package.json b/visualization-core/package.json index 5c4ba27..d47e894 100644 --- a/visualization-core/package.json +++ b/visualization-core/package.json @@ -1,7 +1,7 @@ { "name": "@hediet/visualization-core", "description": "Provides infrastructure to find registered visualizations for given json data", - "version": "0.2.0", + "version": "0.2.1", "license": "MIT", "homepage": "https://github.com/hediet/visualization", "repository": { diff --git a/visualization-core/src/CanvasVisualization.tsx b/visualization-core/src/CanvasVisualization.tsx new file mode 100644 index 0000000..333efe1 --- /dev/null +++ b/visualization-core/src/CanvasVisualization.tsx @@ -0,0 +1,30 @@ +import * as React from "react"; +import { createReactVisualization } from "./ReactVisualization"; +import { Visualization, Visualizer } from "./Visualizer"; +import { Disposable } from "@hediet/std/disposable"; + +export function createCanvas2DVisualization( + sourceVisualizer: Visualizer, + options: { priority: number }, + render: (context: CanvasRenderingContext2D) => Disposable | void +): Visualization { + return createReactVisualization( + sourceVisualizer, + { priority: options.priority }, + () => ( + { + if (canvas) { + const ctx = canvas.getContext("2d")!; + ctx.clearRect(0, 0, canvas.width, canvas.height); + + // We don't want to use try/catch to swallow the error, so we use setTimeout to protect the caller + setTimeout(() => { + render(ctx); + }, 0); + } + }} + /> + ) + ); +} diff --git a/visualization-core/src/ReactVisualization.tsx b/visualization-core/src/ReactVisualization.tsx index 800f6cf..4c83322 100644 --- a/visualization-core/src/ReactVisualization.tsx +++ b/visualization-core/src/ReactVisualization.tsx @@ -7,93 +7,75 @@ import { asVisualizationId, VisualizationRenderOptions, } from "./Visualizer"; +import { Deferred } from "@hediet/std/synchronization"; -interface Options { - id?: string; - name?: string; +interface ReactVisualizationArgs { priority: number; + preload?: (() => Promise) | undefined; } -export abstract class BaseReactVisualization implements Visualization { +class ReactVisualization implements Visualization { public readonly id: VisualizationId; public readonly name: string; public readonly priority: number; + private readonly _preload: (() => Promise) | undefined; constructor( public readonly sourceVisualizer: Visualizer, - options: Options + options: ReactVisualizationArgs, + private readonly getReactNode: ( + options: ReactVisualizationRenderArgs + ) => { + node: React.ReactChild; + ready: Promise; + } ) { - this.id = asVisualizationId( - options.id || sourceVisualizer.id.toString() - ); - this.name = options.name || sourceVisualizer.name; + this.id = asVisualizationId(sourceVisualizer.id.toString()); + this.name = sourceVisualizer.name; this.priority = options.priority; + this._preload = options.preload; + } + + public preload(): Promise { + return this._preload ? this._preload() : Promise.resolve(); } public render( target: HTMLDivElement, options: VisualizationRenderOptions - ): { renderState: unknown } { - const node = this.getReactNode({ + ): { renderState: unknown; ready: Promise } { + const { node, ready } = this.getReactNode({ theme: options.theme, - readyCallback: options.readyCallback, }); // We could use this to get access to theme vars: - //getComputedStyle(target).getPropertyValue("--background-color") + // getComputedStyle(target).getPropertyValue("--background-color") ReactDom.render(<>{node}, target); - return { renderState: undefined }; + return { renderState: undefined, ready }; } - - public abstract preload(): Promise; - protected abstract getReactNode( - options: ReactVisualizationRenderOptions - ): React.ReactChild; } -export interface ReactVisualizationRenderOptions { +export interface ReactVisualizationRenderArgs { theme: Theme; - readyCallback: () => void; -} - -class ReactVisualization extends BaseReactVisualization { - private readonly _preload?: () => Promise; - - constructor( - sourceVisualizer: Visualizer, - options: Options & { preload?: () => Promise }, - private readonly renderReactNode: ( - options: ReactVisualizationRenderOptions - ) => React.ReactChild - ) { - super(sourceVisualizer, options); - this._preload = options.preload; - } - - protected getReactNode( - options: ReactVisualizationRenderOptions - ): React.ReactChild { - return this.renderReactNode(options); - } - - public async preload(): Promise { - if (this._preload) { - return await this._preload(); - } - } } export function createReactVisualization( sourceVisualizer: Visualizer, - options: Options & { preload?: () => Promise }, - renderReactNode: (options: { theme: Theme }) => React.ReactChild + args: ReactVisualizationArgs, + renderReactNode: (args: ReactVisualizationRenderArgs) => React.ReactChild ) { - return new ReactVisualization(sourceVisualizer, options, options => ( - - )); + return new ReactVisualization(sourceVisualizer, args, (options) => { + const deferred = new Deferred(); + return { + node: ( + deferred.resolve()} + child={renderReactNode(options)} + /> + ), + ready: deferred.promise, + }; + }); } class ComponentWithOnMount extends React.Component<{ @@ -111,12 +93,11 @@ class ComponentWithOnMount extends React.Component<{ export function createLazyReactVisualization( sourceVisualizer: Visualizer, - options: Options & { preload?: () => Promise }, - renderReactNode: ( - options: ReactVisualizationRenderOptions - ) => React.ReactChild + args: ReactVisualizationArgs, + renderReactNode: (args: ReactVisualizationRenderArgs) => { + node: React.ReactChild; + ready: Promise; + } ) { - return new ReactVisualization(sourceVisualizer, options, options => - renderReactNode(options) - ); + return new ReactVisualization(sourceVisualizer, args, renderReactNode); } diff --git a/visualization-core/src/RegisterVisualizerFn.ts b/visualization-core/src/RegisterVisualizerFn.ts new file mode 100644 index 0000000..bc40488 --- /dev/null +++ b/visualization-core/src/RegisterVisualizerFn.ts @@ -0,0 +1,26 @@ +import * as semanticJson from "@hediet/semantic-json"; +import { createCanvas2DVisualization } from "./CanvasVisualization"; +import { CreateVisualizerOptions } from "./createVisualizer"; +import { Visualization, Visualizer } from "./Visualizer"; + +export type RegisterVisualizerFn = ( + register: ( + visualizationOptions: CreateVisualizerOptions + ) => void, + lib: Lib +) => void; + +interface Lib { + semanticJson: typeof semanticJson; + + createCanvas2DVisualization: ( + visualizer: Visualizer, + options: { priority: number }, + render: (context: CanvasRenderingContext2D) => void + ) => Visualization; +} + +export const libImplementation: Lib = { + semanticJson, + createCanvas2DVisualization, +}; diff --git a/visualization-core/src/VisualizationData.tsx b/visualization-core/src/VisualizationData.ts similarity index 100% rename from visualization-core/src/VisualizationData.tsx rename to visualization-core/src/VisualizationData.ts diff --git a/visualization-core/src/VisualizationView.tsx b/visualization-core/src/VisualizationView.tsx index 6d831c6..6e489a1 100644 --- a/visualization-core/src/VisualizationView.tsx +++ b/visualization-core/src/VisualizationView.tsx @@ -20,15 +20,15 @@ export class VisualizationView extends React.Component<{ renderVisualization() { const { visualization, theme } = this.props; - const { renderState } = visualization.render(this.ref.current!, { + const { renderState, ready } = visualization.render(this.ref.current!, { theme, previousRenderState: this.visualizationRenderState, - readyCallback: () => { - const r = this.props.onReady; - if (r) { - r(); - } - }, + }); + + ready.then(() => { + if (this.props.onReady) { + return this.props.onReady(); + } }); this.visualizationRenderState = renderState; diff --git a/visualization-core/src/Visualizer.tsx b/visualization-core/src/Visualizer.ts similarity index 62% rename from visualization-core/src/Visualizer.tsx rename to visualization-core/src/Visualizer.ts index 76893f5..65b457a 100644 --- a/visualization-core/src/Visualizer.tsx +++ b/visualization-core/src/Visualizer.ts @@ -1,18 +1,48 @@ import { BaseSerializer } from "@hediet/semantic-json"; import { Theme } from "./Theme"; +export interface Visualizer { + readonly id: VisualizerId; + readonly name: string; + + /** + * Allows to deserialize JSON into an visualization. + * This object is also used to inspect the schema of that JSON. + */ + readonly serializer: BaseSerializer; +} + export interface VisualizerId extends String { __brand: "VisualizerId"; } export function asVisualizerId(id: string): VisualizerId { - return (id as unknown) as VisualizerId; + return id as unknown as VisualizerId; } -export interface Visualizer { - readonly id: VisualizerId; +export interface Visualization { + readonly id: VisualizationId; readonly name: string; - readonly serializer: BaseSerializer; + readonly priority: number; + + render( + target: HTMLDivElement, + options: VisualizationRenderOptions + ): { + renderState: TRenderState; + /** + * Is resolved when the visualization stabilized and is ready to be shown. + * Note that the visualization can be shown before this promise resolves. + * This promise is useful for loading animations or integrations. + */ + ready: Promise; + }; + + /** + * Preloads resources to speed up first rendering. + * The visualization does not need to be rendered for this. + */ + preload(): Promise; } export interface VisualizationId extends String { @@ -20,28 +50,10 @@ export interface VisualizationId extends String { } export function asVisualizationId(id: string): VisualizationId { - return (id as unknown) as VisualizationId; + return id as unknown as VisualizationId; } export interface VisualizationRenderOptions { theme: Theme; previousRenderState: TRenderState | undefined; - readyCallback: () => void; -} - -export interface Visualization { - readonly id: VisualizationId; - readonly name: string; - readonly priority: number; - readonly sourceVisualizer: Visualizer; - - render( - target: HTMLDivElement, - options: VisualizationRenderOptions - ): { renderState: TRenderState }; - - /** - * Preloads resources to speed up first rendering. - */ - preload(): Promise; } diff --git a/visualization-core/src/VisualizerRegistry.ts b/visualization-core/src/VisualizerRegistry.ts index 74fd4ed..a4ac2eb 100644 --- a/visualization-core/src/VisualizerRegistry.ts +++ b/visualization-core/src/VisualizerRegistry.ts @@ -16,7 +16,7 @@ export class VisualizationFactory { private _getSerializer(map: ObservableMap) { return sUnionMany( - [...map.values()].map(v => v.serializer.asSerializer()), + [...map.values()].map((v) => v.serializer.asSerializer()), { processingStrategy: "all" } ); } @@ -25,6 +25,14 @@ export class VisualizationFactory { this.visualizers.set(visualizer.id.toString(), visualizer); } + public getRegisteredVisualizers(): Visualizer[] { + return [...this.visualizers.values()]; + } + + public getRegisteredHiddenVisualizer(): Visualizer[] { + return [...this.hiddenVisualizers.values()]; + } + /** * Hidden visualizers don't appear in `getSerializer` and should only be presented to the user if he explicitly selects it. * `getVisualizations(...).bestVisualization` is only the result of a hidden visualizer if the user requests a hidden visualization id. @@ -38,11 +46,11 @@ export class VisualizationFactory { preferredVisualization: VisualizationId | undefined ): Visualizations { const u = this.getSerializer(); - const result = u.deserialize((data as unknown) as JSONValue); + const result = u.deserialize(data as unknown as JSONValue); const allVisualizations = result.value || []; const u2 = this._getSerializer(this.hiddenVisualizers); - const hiddenResult = u2.deserialize((data as unknown) as JSONValue); + const hiddenResult = u2.deserialize(data as unknown as JSONValue); allVisualizations.push(...(hiddenResult.value || [])); allVisualizations.sort((a, b) => b.priority - a.priority); @@ -53,7 +61,7 @@ export class VisualizationFactory { } if (preferredVisualization) { const preferred = allVisualizations.find( - vis => vis.id === preferredVisualization + (vis) => vis.id === preferredVisualization ); if (preferred) { bestVisualization = preferred; diff --git a/visualization-core/src/createVisualizer.tsx b/visualization-core/src/createVisualizer.ts similarity index 75% rename from visualization-core/src/createVisualizer.tsx rename to visualization-core/src/createVisualizer.ts index 9d2c0d3..de732a7 100644 --- a/visualization-core/src/createVisualizer.tsx +++ b/visualization-core/src/createVisualizer.ts @@ -1,24 +1,30 @@ import { BaseSerializer } from "@hediet/semantic-json"; import { Visualization, Visualizer, asVisualizerId } from "./Visualizer"; -export function createVisualizer(options: { +export interface CreateVisualizerOptions { /** * A unique id for this visualizer. */ id: string; + /** * A human readable name of this visualizer. * Should be in English language. */ name: string; + serializer: BaseSerializer; getVisualization: (data: TData, self: Visualizer) => Visualization; -}): Visualizer { +} + +export function createVisualizer( + options: CreateVisualizerOptions +): Visualizer { const result: Visualizer = { id: asVisualizerId(options.id), name: options.name, serializer: options.serializer.refine({ - fromIntermediate: data => options.getVisualization(data, result), + fromIntermediate: (data) => options.getVisualization(data, result), canSerialize: (val): val is Visualization => false, toIntermediate: () => { throw new Error("not supported"); diff --git a/visualization-core/src/index.tsx b/visualization-core/src/index.tsx index 1e8f3be..57c1c31 100644 --- a/visualization-core/src/index.tsx +++ b/visualization-core/src/index.tsx @@ -5,3 +5,4 @@ export * from "./VisualizerRegistry"; export * from "./VisualizationView"; export * from "./Theme"; export * from "./createVisualizer"; +export * from "./RegisterVisualizerFn";