initial commit

This commit is contained in:
Henning Dieterichs
2020-08-12 23:31:12 +02:00
commit 7c4c9348f3
98 changed files with 15500 additions and 0 deletions

View 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);
}
}

View 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"
) {}
}

View 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);
}

View 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}
/>
);
}
}

View 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;
}

View 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();

View 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;
}

View 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";