initial commit
This commit is contained in:
56
visualization-core/src/ReactVisualization.tsx
Normal file
56
visualization-core/src/ReactVisualization.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
import * as React from "react";
|
||||
import * as ReactDom from "react-dom";
|
||||
import { Visualization, Theme } from "./index";
|
||||
import { Visualizer, VisualizationId, asVisualizationId } from "./Visualizer";
|
||||
|
||||
interface Options {
|
||||
id?: string;
|
||||
name?: string;
|
||||
priority: number;
|
||||
}
|
||||
|
||||
export abstract class BaseReactVisualization implements Visualization<unknown> {
|
||||
public readonly id: VisualizationId;
|
||||
public readonly name: string;
|
||||
public readonly priority: number;
|
||||
|
||||
constructor(
|
||||
public readonly sourceVisualizer: Visualizer,
|
||||
options: Options
|
||||
) {
|
||||
this.id = asVisualizationId(
|
||||
options.id || sourceVisualizer.id.toString()
|
||||
);
|
||||
this.name = options.name || sourceVisualizer.name;
|
||||
this.priority = options.priority;
|
||||
}
|
||||
|
||||
public render(
|
||||
target: HTMLDivElement,
|
||||
theme: Theme,
|
||||
previous: unknown | undefined
|
||||
): unknown {
|
||||
const node = this.getReactNode(theme);
|
||||
// We could use this to get access to theme vars:
|
||||
// getComputedStyle(target).getPropertyValue("--background-color")
|
||||
|
||||
ReactDom.render(<>{node}</>, target);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
protected abstract getReactNode(theme: Theme): React.ReactChild;
|
||||
}
|
||||
|
||||
export class ReactVisualization extends BaseReactVisualization {
|
||||
constructor(
|
||||
sourceVisualizer: Visualizer,
|
||||
options: Options,
|
||||
private readonly renderReactNode: (theme: Theme) => React.ReactChild
|
||||
) {
|
||||
super(sourceVisualizer, options);
|
||||
}
|
||||
|
||||
protected getReactNode(theme: Theme): React.ReactChild {
|
||||
return this.renderReactNode(theme);
|
||||
}
|
||||
}
|
||||
9
visualization-core/src/Theme.ts
Normal file
9
visualization-core/src/Theme.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export class Theme {
|
||||
public static readonly light = new Theme("light", "light");
|
||||
public static readonly dark = new Theme("dark", "dark");
|
||||
|
||||
constructor(
|
||||
public readonly id: string,
|
||||
public readonly kind: "light" | "dark"
|
||||
) {}
|
||||
}
|
||||
16
visualization-core/src/VisualizationData.tsx
Normal file
16
visualization-core/src/VisualizationData.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
export interface VisualizationData {
|
||||
kind: Record<string, true>;
|
||||
}
|
||||
|
||||
export function isVisualizationData(val: unknown): val is VisualizationData {
|
||||
if (typeof val !== "object" || !val || !("kind" in val)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const obj = val as any;
|
||||
if (typeof obj.kind !== "object" || !obj.kind) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return Object.values(obj.kind).every(val => val === true);
|
||||
}
|
||||
42
visualization-core/src/VisualizationView.tsx
Normal file
42
visualization-core/src/VisualizationView.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import * as React from "react";
|
||||
import { Visualization } from "./Visualizer";
|
||||
import { Theme } from "./Theme";
|
||||
|
||||
export class VisualizationView extends React.Component<{
|
||||
visualization: Visualization;
|
||||
theme: Theme;
|
||||
}> {
|
||||
private readonly ref = React.createRef<HTMLDivElement>();
|
||||
private visualizationRenderState: unknown = undefined;
|
||||
|
||||
componentDidMount() {
|
||||
this.renderVisualization();
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
this.renderVisualization();
|
||||
}
|
||||
|
||||
renderVisualization() {
|
||||
const { visualization, theme } = this.props;
|
||||
this.visualizationRenderState = visualization.render(
|
||||
this.ref.current!,
|
||||
theme,
|
||||
this.visualizationRenderState
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div
|
||||
className={`themeable theme-${this.props.theme.id} visualization`}
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
boxSizing: "border-box",
|
||||
}}
|
||||
ref={this.ref}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
37
visualization-core/src/Visualizer.tsx
Normal file
37
visualization-core/src/Visualizer.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import { BaseSerializer } from "@hediet/semantic-json";
|
||||
import { Theme } from "./Theme";
|
||||
|
||||
export interface VisualizerId extends String {
|
||||
__brand: "VisualizerId";
|
||||
}
|
||||
|
||||
export function asVisualizerId(id: string): VisualizerId {
|
||||
return (id as unknown) as VisualizerId;
|
||||
}
|
||||
|
||||
export interface Visualizer {
|
||||
readonly id: VisualizerId;
|
||||
readonly name: string;
|
||||
readonly serializer: BaseSerializer<Visualization>;
|
||||
}
|
||||
|
||||
export interface VisualizationId extends String {
|
||||
__brand: "VisualizationId";
|
||||
}
|
||||
|
||||
export function asVisualizationId(id: string): VisualizationId {
|
||||
return (id as unknown) as VisualizationId;
|
||||
}
|
||||
|
||||
export interface Visualization<TRenderState = any> {
|
||||
readonly id: VisualizationId;
|
||||
readonly name: string;
|
||||
readonly priority: number;
|
||||
readonly sourceVisualizer: Visualizer;
|
||||
|
||||
render(
|
||||
target: HTMLDivElement,
|
||||
theme: Theme,
|
||||
previousRenderState: TRenderState | undefined
|
||||
): TRenderState;
|
||||
}
|
||||
82
visualization-core/src/VisualizerRegistry.ts
Normal file
82
visualization-core/src/VisualizerRegistry.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import { Visualizer, Visualization, VisualizationId } from "./Visualizer";
|
||||
import { VisualizationData } from "./VisualizationData";
|
||||
import { JSONValue, sUnionMany } from "@hediet/semantic-json";
|
||||
import { ObservableMap } from "mobx";
|
||||
|
||||
export class VisualizationFactory {
|
||||
private readonly visualizers = new ObservableMap<string, Visualizer>();
|
||||
private readonly hiddenVisualizers = new ObservableMap<
|
||||
string,
|
||||
Visualizer
|
||||
>();
|
||||
|
||||
public getSerializer() {
|
||||
return this._getSerializer(this.visualizers);
|
||||
}
|
||||
|
||||
private _getSerializer(map: ObservableMap<string, Visualizer>) {
|
||||
return sUnionMany(
|
||||
[...map.values()].map(v => v.serializer.asSerializer()),
|
||||
{ processingStrategy: "all" }
|
||||
);
|
||||
}
|
||||
|
||||
public addVisualizer(visualizer: Visualizer): void {
|
||||
this.visualizers.set(visualizer.id.toString(), visualizer);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
public addHiddenVisualizer(visualizer: Visualizer): void {
|
||||
this.hiddenVisualizers.set(visualizer.id.toString(), visualizer);
|
||||
}
|
||||
|
||||
public getVisualizations(
|
||||
data: VisualizationData,
|
||||
preferredVisualization: VisualizationId | undefined
|
||||
): Visualizations {
|
||||
const u = this.getSerializer();
|
||||
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);
|
||||
allVisualizations.push(...(hiddenResult.value || []));
|
||||
|
||||
allVisualizations.sort((a, b) => b.priority - a.priority);
|
||||
|
||||
let bestVisualization: Visualization | undefined = allVisualizations[0];
|
||||
if (bestVisualization && bestVisualization.priority < 0) {
|
||||
bestVisualization = undefined;
|
||||
}
|
||||
if (preferredVisualization) {
|
||||
const preferred = allVisualizations.find(
|
||||
vis => vis.id === preferredVisualization
|
||||
);
|
||||
if (preferred) {
|
||||
bestVisualization = preferred;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
bestVisualization,
|
||||
allVisualizations,
|
||||
visualizationDataErrors: [],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export interface Visualizations {
|
||||
bestVisualization: Visualization | undefined;
|
||||
allVisualizations: Visualization[];
|
||||
visualizationDataErrors: VisualizationDataError[];
|
||||
}
|
||||
|
||||
export interface VisualizationDataError {
|
||||
visualizer: Visualizer;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export const globalVisualizationFactory = new VisualizationFactory();
|
||||
29
visualization-core/src/createVisualizer.tsx
Normal file
29
visualization-core/src/createVisualizer.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import { BaseSerializer } from "@hediet/semantic-json";
|
||||
import { Visualization, Visualizer, asVisualizerId } from "./Visualizer";
|
||||
|
||||
export function createVisualizer<TData>(options: {
|
||||
/**
|
||||
* 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 {
|
||||
const result: Visualizer = {
|
||||
id: asVisualizerId(options.id),
|
||||
name: options.name,
|
||||
serializer: options.serializer.refine({
|
||||
fromIntermediate: data => options.getVisualization(data, result),
|
||||
canSerialize: (val): val is Visualization => false,
|
||||
toIntermediate: () => {
|
||||
throw new Error("not supported");
|
||||
},
|
||||
}),
|
||||
};
|
||||
return result;
|
||||
}
|
||||
7
visualization-core/src/index.tsx
Normal file
7
visualization-core/src/index.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
export * from "./VisualizationData";
|
||||
export * from "./ReactVisualization";
|
||||
export * from "./Visualizer";
|
||||
export * from "./VisualizerRegistry";
|
||||
export * from "./VisualizationView";
|
||||
export * from "./Theme";
|
||||
export * from "./createVisualizer";
|
||||
Reference in New Issue
Block a user