Adds support for external visualizer functions.
This commit is contained in:
@@ -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": {
|
||||
|
||||
30
visualization-core/src/CanvasVisualization.tsx
Normal file
30
visualization-core/src/CanvasVisualization.tsx
Normal 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);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
26
visualization-core/src/RegisterVisualizerFn.ts
Normal file
26
visualization-core/src/RegisterVisualizerFn.ts
Normal 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,
|
||||
};
|
||||
@@ -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;
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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");
|
||||
@@ -5,3 +5,4 @@ export * from "./VisualizerRegistry";
|
||||
export * from "./VisualizationView";
|
||||
export * from "./Theme";
|
||||
export * from "./createVisualizer";
|
||||
export * from "./RegisterVisualizerFn";
|
||||
|
||||
Reference in New Issue
Block a user