Adds support for external visualizer functions.

This commit is contained in:
Henning Dieterichs
2023-04-21 11:52:30 +02:00
parent ac15ceb29c
commit 96115a9a70
13 changed files with 319 additions and 158 deletions

View File

@@ -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"; // */

View File

@@ -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<string, number>();
// 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(
`
<mxCell id="${nodeId}" value="${label}" style="${style}" vertex="1" parent="1">
<mxGeometry x="${node.x}" y="${node.y}" width="${node.shape.width}" height="${node.shape.height}" as="geometry"/>
</mxCell>`
);
}
for (const edge of Object.values(n.body.edges) as any) {
const label = edge.options.label || "";
const edgeId = id++;
nodesText.push(
`
<mxCell id="${edgeId}" value="${label}" edge="1" source="${visJsIdToNodeId.get(
edge.from.id
)}" target="${visJsIdToNodeId.get(edge.to.id)}" parent="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>`
);
}
const data = `
<mxGraphModel>
<root>
<mxCell id="0"/>
<mxCell id="1" parent="0"/>
${nodesText.join("\n")}
</root>
</mxGraphModel>`;
/*
<mxCell id="2" value="xxx" edge="1" source="3" target="4" parent="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
*/
e.clipboardData!.setData("text/plain", encodeURIComponent(data));
e.preventDefault();
};
componentWillUnmount() {
document.removeEventListener("copy", this.onCopy);
}
}

View File

@@ -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 }) => (
<PlotlyViewerLazyLoadable
data={data.data}
layout={data.layout}
theme={theme}
onReady={readyCallback}
/>
)
({ theme }) => {
const b = new Deferred();
return {
node: (
<PlotlyViewerLazyLoadable
data={data.data}
layout={data.layout}
theme={theme}
onReady={() => b.resolve}
/>
),
ready: b.promise,
};
}
),
});

View File

@@ -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": {

View File

@@ -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 },
() => (
<canvas
ref={(canvas) => {
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);
}
}}
/>
)
);
}

View File

@@ -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<void>) | undefined;
}
export abstract class BaseReactVisualization implements Visualization<unknown> {
class ReactVisualization implements Visualization<unknown> {
public readonly id: VisualizationId;
public readonly name: string;
public readonly priority: number;
private readonly _preload: (() => Promise<void>) | undefined;
constructor(
public readonly sourceVisualizer: Visualizer,
options: Options
options: ReactVisualizationArgs,
private readonly getReactNode: (
options: ReactVisualizationRenderArgs
) => {
node: React.ReactChild;
ready: Promise<void>;
}
) {
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<void> {
return this._preload ? this._preload() : Promise.resolve();
}
public render(
target: HTMLDivElement,
options: VisualizationRenderOptions<unknown>
): { renderState: unknown } {
const node = this.getReactNode({
): { renderState: unknown; ready: Promise<void> } {
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<void>;
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<void>;
constructor(
sourceVisualizer: Visualizer,
options: Options & { preload?: () => Promise<void> },
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<void> {
if (this._preload) {
return await this._preload();
}
}
}
export function createReactVisualization(
sourceVisualizer: Visualizer,
options: Options & { preload?: () => Promise<void> },
renderReactNode: (options: { theme: Theme }) => React.ReactChild
args: ReactVisualizationArgs,
renderReactNode: (args: ReactVisualizationRenderArgs) => React.ReactChild
) {
return new ReactVisualization(sourceVisualizer, options, options => (
<ComponentWithOnMount
onDidMount={options.readyCallback}
child={renderReactNode(options)}
/>
));
return new ReactVisualization(sourceVisualizer, args, (options) => {
const deferred = new Deferred();
return {
node: (
<ComponentWithOnMount
onDidMount={() => 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<void> },
renderReactNode: (
options: ReactVisualizationRenderOptions
) => React.ReactChild
args: ReactVisualizationArgs,
renderReactNode: (args: ReactVisualizationRenderArgs) => {
node: React.ReactChild;
ready: Promise<void>;
}
) {
return new ReactVisualization(sourceVisualizer, options, options =>
renderReactNode(options)
);
return new ReactVisualization(sourceVisualizer, args, renderReactNode);
}

View File

@@ -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: <TData>(
visualizationOptions: CreateVisualizerOptions<TData>
) => 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,
};

View File

@@ -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;

View File

@@ -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<Visualization>;
}
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<TRenderState = any> {
readonly id: VisualizationId;
readonly name: string;
readonly serializer: BaseSerializer<Visualization>;
readonly priority: number;
render(
target: HTMLDivElement,
options: VisualizationRenderOptions<TRenderState>
): {
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<void>;
};
/**
* Preloads resources to speed up first rendering.
* The visualization does not need to be rendered for this.
*/
preload(): Promise<void>;
}
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<TRenderState> {
theme: Theme;
previousRenderState: TRenderState | undefined;
readyCallback: () => void;
}
export interface Visualization<TRenderState = any> {
readonly id: VisualizationId;
readonly name: string;
readonly priority: number;
readonly sourceVisualizer: Visualizer;
render(
target: HTMLDivElement,
options: VisualizationRenderOptions<TRenderState>
): { renderState: TRenderState };
/**
* Preloads resources to speed up first rendering.
*/
preload(): Promise<void>;
}

View File

@@ -16,7 +16,7 @@ export class VisualizationFactory {
private _getSerializer(map: ObservableMap<string, Visualizer>) {
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;

View File

@@ -1,24 +1,30 @@
import { BaseSerializer } from "@hediet/semantic-json";
import { Visualization, Visualizer, asVisualizerId } from "./Visualizer";
export function createVisualizer<TData>(options: {
export interface CreateVisualizerOptions<TData> {
/**
* A unique id for this visualizer.
*/
id: string;
/**
* A human readable name of this visualizer.
* Should be in English language.
*/
name: string;
serializer: BaseSerializer<TData>;
getVisualization: (data: TData, self: Visualizer) => Visualization;
}): Visualizer {
}
export function createVisualizer<TData>(
options: CreateVisualizerOptions<TData>
): 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");

View File

@@ -5,3 +5,4 @@ export * from "./VisualizerRegistry";
export * from "./VisualizationView";
export * from "./Theme";
export * from "./createVisualizer";
export * from "./RegisterVisualizerFn";