├── .github └── workflows │ ├── ci.yml │ └── playground.yml ├── .gitignore ├── .prettierrc.json ├── .vscode ├── launch.json └── settings.json ├── LICENSE.md ├── README.md ├── package.json ├── playground ├── package.json ├── scripts │ └── build-docs.ts ├── src │ ├── common │ │ ├── hotComponent.tsx │ │ └── utils.ts │ ├── components │ │ ├── App.tsx │ │ ├── DemoView.tsx │ │ ├── EditorView.tsx │ │ ├── JsonEditor.tsx │ │ ├── MainView.tsx │ │ ├── PlaygroundDocumentEditor.tsx │ │ ├── Select.tsx │ │ ├── VSCodeBrigde.tsx │ │ ├── VisualizationWithEditorView.tsx │ │ └── modelCount.ts │ ├── demo.editable.json │ ├── index.tsx │ ├── model │ │ ├── JsonDataSource.tsx │ │ ├── Model.ts │ │ ├── MonacoBridge.ts │ │ ├── QueryController.ts │ │ ├── VisualizationDataSource.ts │ │ ├── lzmaCompressor.ts │ │ └── types.d.ts │ ├── style.scss │ ├── visualizations.ts │ ├── vscode-dark.scss │ └── vscode-light.scss ├── tsconfig.json └── webpack.config.ts ├── simple-demo ├── package.json ├── src │ ├── index.tsx │ └── style.scss ├── tsconfig.json └── webpack.config.ts ├── tslint.json ├── visualization-bundle ├── README.md ├── package.json ├── src │ ├── consts.ts │ ├── index.ts │ ├── types.d.ts │ ├── utils │ │ ├── LazyLoadable.tsx │ │ ├── Loadable.tsx │ │ └── index.ts │ ├── vars.scss │ └── visualizers │ │ ├── ast-visualizer │ │ ├── AstVisualizer.tsx │ │ ├── index.tsx │ │ ├── style.scss │ │ └── types.d.ts │ │ ├── graph │ │ ├── dot-graphviz-visualizer │ │ │ ├── GraphvizDotVisualizer.tsx │ │ │ ├── index.tsx │ │ │ └── types.d.ts │ │ ├── graph-graphviz-visualizer │ │ │ ├── getGraphVizData.tsx │ │ │ └── index.tsx │ │ ├── graph-visjs-visualizer │ │ │ ├── VisJsGraphViewer.tsx │ │ │ ├── index.tsx │ │ │ └── style.scss │ │ ├── index.ts │ │ └── sGraph.ts │ │ ├── grid-visualizer │ │ ├── GridVisualizer.tsx │ │ ├── index.tsx │ │ └── style.scss │ │ ├── image-visualizer │ │ └── index.tsx │ │ ├── index.ts │ │ ├── monaco-text-diff-visualizer │ │ ├── MonacoEditor.tsx │ │ ├── index.tsx │ │ └── style.scss │ │ ├── monaco-text-visualizer │ │ ├── MonacoEditor.tsx │ │ ├── getLanguageId.tsx │ │ ├── index.tsx │ │ └── style.scss │ │ ├── perspective-table-visualizer │ │ ├── PerspectiveDataViewer.tsx │ │ └── index.tsx │ │ ├── plotly-visualizer │ │ ├── PlotlyViewer.tsx │ │ ├── index.tsx │ │ └── style.scss │ │ ├── simple-text-visualizer │ │ ├── index.tsx │ │ └── style.scss │ │ ├── source-visualizer │ │ ├── index.tsx │ │ └── style.scss │ │ ├── svg-visualizer │ │ ├── SvgViewer.tsx │ │ ├── index.tsx │ │ └── style.scss │ │ └── tree-visualizer │ │ ├── Point.ts │ │ ├── SvgElements.tsx │ │ ├── Views.tsx │ │ ├── index.tsx │ │ └── style.scss ├── style.scss └── tsconfig.json ├── visualization-core ├── README.md ├── architecture.drawio.svg ├── package.json ├── src │ ├── CanvasVisualization.tsx │ ├── ReactVisualization.tsx │ ├── RegisterVisualizerFn.ts │ ├── Theme.ts │ ├── VisualizationData.ts │ ├── VisualizationView.tsx │ ├── Visualizer.ts │ ├── VisualizerRegistry.ts │ ├── createVisualizer.ts │ └── index.tsx └── tsconfig.json └── yarn.lock /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - master 5 | pull_request: 6 | 7 | jobs: 8 | build: 9 | strategy: 10 | matrix: 11 | os: [macos-latest, ubuntu-latest, windows-latest] 12 | runs-on: ${{ matrix.os }} 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v2 16 | with: 17 | submodules: true 18 | - name: Install Node.js 19 | uses: actions/setup-node@v1 20 | with: 21 | node-version: 10.x 22 | - run: yarn install 23 | - run: yarn build 24 | -------------------------------------------------------------------------------- /.github/workflows/playground.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | branches: 5 | - master 6 | jobs: 7 | release: 8 | name: Release 9 | runs-on: ubuntu-18.04 10 | steps: 11 | - name: Checkout 12 | uses: actions/checkout@v1 13 | - name: Setup Node.js 14 | uses: actions/setup-node@v1 15 | with: 16 | node-version: 12 17 | - name: Install dependencies 18 | run: yarn install 19 | - name: Build 20 | run: yarn build 21 | - name: Publish github pages 22 | run: | 23 | git remote set-url origin https://git:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git 24 | yarn workspace playground run pub 25 | env: 26 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | *.pid.lock 11 | 12 | # Directory for instrumented libs generated by jscoverage/JSCover 13 | lib-cov 14 | 15 | # Coverage directory used by tools like istanbul 16 | coverage 17 | 18 | # nyc test coverage 19 | .nyc_output 20 | 21 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 22 | .grunt 23 | 24 | # node-waf configuration 25 | .lock-wscript 26 | 27 | # Compiled binary addons (http://nodejs.org/api/addons.html) 28 | build/Release 29 | 30 | # Dependency directories 31 | node_modules 32 | jspm_packages 33 | 34 | # Optional npm cache directory 35 | .npm 36 | 37 | # Optional REPL history 38 | .node_repl_history 39 | 40 | dist 41 | api 42 | 43 | dist/ 44 | 45 | .npmrc 46 | 47 | .yarn/cache 48 | .yarn/unplugged 49 | .yarn/build-state.yml 50 | .yarn/install-state.gz 51 | .pnp.js 52 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 4, 4 | "semi": true, 5 | "useTabs": true 6 | } 7 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "type": "pwa-chrome", 6 | "request": "launch", 7 | "name": "Launch Chrome against localhost", 8 | "url": "http://localhost:8080", 9 | "webRoot": "${workspaceFolder}/playground" 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "tasksStatusbar.taskLabelFilter": "dev", 3 | "editor.formatOnSave": true, 4 | "typescript.tsdk": "node_modules\\typescript\\lib" 5 | } 6 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Henning Dieterichs 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Visualization Framework 2 | 3 | This repository contains two packages. 4 | 5 | ## @hediet/visualization-core 6 | 7 | This package provides basic infrastructure for implementing visualizations. 8 | See its readme [here](./visualization-core/README.md). 9 | 10 | ## @hediet/visualization-bundle 11 | 12 | This package bundles all the visualizations. 13 | See its readme [here](./visualization-bundle/README.md). 14 | 15 | If you want to add your own visualization to this project, visualization-bundle is the right place! 16 | The readme documents how to implement your own visualization. 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "workspaces": [ 4 | "./visualization-core", 5 | "./visualization-bundle", 6 | "./simple-demo", 7 | "./playground" 8 | ], 9 | "scripts": { 10 | "build": "yarn build-core && yarn build-bundle && yarn build-playground && yarn build-simple-demo", 11 | "build-core": "yarn workspace @hediet/visualization-core run build", 12 | "build-bundle": "yarn workspace @hediet/visualization-bundle run build", 13 | "build-playground": "yarn workspace playground run build", 14 | "build-simple-demo": "yarn workspace simple-demo run build" 15 | }, 16 | "devDependencies": { 17 | "prettier": "^1.19.1" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /playground/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "playground", 3 | "version": "0.0.1", 4 | "license": "MIT", 5 | "scripts": { 6 | "dev": "webpack-dev-server --hot", 7 | "build": "yarn build-webpack && yarn build-docs", 8 | "build-webpack": "node --max_old_space_size=4096 --openssl-legacy-provider ../node_modules/webpack/bin/webpack.js --mode production", 9 | "build-docs": "ts-node ./scripts/build-docs", 10 | "pub": "gh-pages -d dist -u 'github-actions-bot '" 11 | }, 12 | "dependencies": { 13 | "@blueprintjs/core": "^3.23.1", 14 | "@hediet/std": "^0.6.0", 15 | "@hediet/semantic-json": "^0.3.14", 16 | "blueprintjs": "^0.0.8", 17 | "classnames": "^2.2.6", 18 | "mobx": "^5.15.4", 19 | "mobx-react": "^6.1.8", 20 | "monaco-editor": "^0.25.2", 21 | "react": "^16.12.0", 22 | "react-dom": "^16.12.0", 23 | "messagepack": "^1.1.10", 24 | "base64-js": "^1.3.1", 25 | "lzma": "^2.3.2", 26 | "@hediet/monaco-editor-react": "^0.2.0" 27 | }, 28 | "devDependencies": { 29 | "gh-pages": "^2.2.0", 30 | "@types/html-webpack-plugin": "^3.2.2", 31 | "@types/webpack": "^4.41.6", 32 | "@types/classnames": "^2.2.9", 33 | "@types/react": "^16.9.22", 34 | "@types/react-dom": "^16.9.5", 35 | "clean-webpack-plugin": "^3.0.0", 36 | "css-loader": "^3.4.2", 37 | "file-loader": "^5.1.0", 38 | "fork-ts-checker-webpack-plugin": "^4.0.4", 39 | "html-webpack-plugin": "^3.2.0", 40 | "monaco-editor-webpack-plugin": "^4.0.0", 41 | "raw-loader": "^4.0.0", 42 | "sass-loader": "^8.0.2", 43 | "sass": "^1.25.0", 44 | "style-loader": "^1.1.3", 45 | "ts-loader": "^6.2.1", 46 | "ts-node": "^8.6.2", 47 | "typescript": "^3.8.2", 48 | "webpack": "^4.41.6", 49 | "webpack-cli": "^3.3.11", 50 | "webpack-dev-server": "^3.10.3", 51 | "@types/base64-js": "^1.3.0", 52 | "source-map-loader": "^1.0.1", 53 | "less-loader": "^6.2.0" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /playground/scripts/build-docs.ts: -------------------------------------------------------------------------------- 1 | import { 2 | JsonSchemaGenerator, 3 | TypeScriptTypeGenerator, 4 | } from "@hediet/semantic-json"; 5 | import { writeFileSync, mkdirSync } from "fs"; 6 | import { join } from "path"; 7 | import { globalVisualizationFactory } from "@hediet/visualization-core"; 8 | import "../src/visualizations"; 9 | 10 | const tsGen = new TypeScriptTypeGenerator(); 11 | const tp = tsGen.getType(globalVisualizationFactory.getSerializer()); 12 | 13 | const tsSrc = `export type KnownVisualizationData = ${tp};\n\n\n${[ 14 | ...tsGen.definitions.values(), 15 | ] 16 | .map(v => v.getDefinitionSource({ exported: true }) + `\n\n`) 17 | .join("")}`; 18 | 19 | const targetDir = join(__dirname, "../dist/docs"); 20 | 21 | mkdirSync(targetDir, { recursive: true }); 22 | 23 | writeFileSync(join(targetDir, "visualization-data.ts"), tsSrc, { 24 | encoding: "utf-8", 25 | }); 26 | 27 | const schema = new JsonSchemaGenerator().getJsonSchemaWithDefinitions( 28 | globalVisualizationFactory.getSerializer() 29 | ); 30 | const jsonSchema = JSON.stringify(schema, undefined, 4); 31 | 32 | writeFileSync(join(targetDir, "visualization-data-schema.json"), jsonSchema, { 33 | encoding: "utf-8", 34 | }); 35 | -------------------------------------------------------------------------------- /playground/src/common/hotComponent.tsx: -------------------------------------------------------------------------------- 1 | import Module = require("module"); 2 | import { observer } from "mobx-react"; 3 | import * as React from "react"; 4 | import { observable, runInAction } from "mobx"; 5 | 6 | type ReactComponent

= 7 | | React.ComponentClass

8 | | React.FunctionComponent

; 9 | 10 | const allComponents = new Map(); 11 | 12 | export function hotComponent( 13 | module: Module 14 | ): (Component: T) => T { 15 | return (component: T): T => { 16 | const key = JSON.stringify({ id: module.id, name: component.name }); 17 | 18 | let result = allComponents.get(key); 19 | if (!result) { 20 | result = observable({ component: component }); 21 | allComponents.set(key, result); 22 | } else { 23 | setTimeout(() => { 24 | runInAction(`Update Component ${component.name}`, () => { 25 | result!.component = component; 26 | }); 27 | }, 0); 28 | } 29 | 30 | const m = module as { 31 | hot?: { 32 | accept: (( 33 | componentName: string, 34 | callback: () => void 35 | ) => void) & 36 | ((callback: () => void) => void); 37 | }; 38 | }; 39 | 40 | if (m.hot) { 41 | m.hot.accept(() => {}); 42 | } 43 | 44 | return observer((props: any) => { 45 | const C = result!.component; 46 | return ; 47 | }) as any; 48 | }; 49 | } 50 | -------------------------------------------------------------------------------- /playground/src/common/utils.ts: -------------------------------------------------------------------------------- 1 | import { Disposable } from "@hediet/std/disposable"; 2 | import { observable } from "mobx"; 3 | import { wait } from "@hediet/std/timer"; 4 | 5 | export function ref( 6 | obj: T, 7 | key: TKey 8 | ): Ref { 9 | return new Ref( 10 | () => obj[key], 11 | v => (obj[key] = v) 12 | ); 13 | } 14 | 15 | export interface IRef { 16 | get: () => T; 17 | set: (value: T) => void; 18 | } 19 | 20 | export class Ref implements IRef { 21 | constructor( 22 | public readonly get: () => T, 23 | public readonly set: (value: T) => void 24 | ) {} 25 | 26 | public map(to: (t: T) => TNew, from: (tNew: TNew) => T): Ref { 27 | return new Ref( 28 | () => to(this.get()), 29 | val => this.set(from(val)) 30 | ); 31 | } 32 | } 33 | 34 | export class Animator { 35 | public readonly dispose = Disposable.fn(); 36 | 37 | @observable public index: number = 0; 38 | 39 | constructor(private readonly times: number[]) { 40 | if (times.length > 1) { 41 | this.run(); 42 | } 43 | } 44 | 45 | private async run() { 46 | let index = 0; 47 | while (true) { 48 | await wait(this.times[index]); 49 | index = (index + 1) % this.times.length; 50 | this.index = index; 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /playground/src/components/App.tsx: -------------------------------------------------------------------------------- 1 | import React = require("react"); 2 | import { hotComponent } from "../common/hotComponent"; 3 | import { Model } from "../model/Model"; 4 | import { MainView } from "./MainView"; 5 | import { observable, runInAction } from "mobx"; 6 | import { observer } from "mobx-react"; 7 | import "../visualizations"; 8 | import { loadMonaco, getMonaco } from "@hediet/monaco-editor-react"; 9 | 10 | @hotComponent(module) 11 | @observer 12 | export class App extends React.Component { 13 | @observable model: Model | undefined; 14 | 15 | constructor(props: {}) { 16 | super(props); 17 | this.init(); 18 | } 19 | 20 | async init() { 21 | if (!getMonaco()) { 22 | await loadMonaco(); 23 | } 24 | runInAction(() => { 25 | this.model = new Model(); 26 | }); 27 | } 28 | 29 | render() { 30 | if (!this.model) { 31 | return

Loading...
; 32 | } 33 | return ; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /playground/src/components/DemoView.tsx: -------------------------------------------------------------------------------- 1 | import { hotComponent } from "../common/hotComponent"; 2 | import { observer } from "mobx-react"; 3 | import React = require("react"); 4 | import { PlaygroundDocumentEditor } from "./PlaygroundDocumentEditor"; 5 | import { observable } from "mobx"; 6 | import { Model } from "../model/Model"; 7 | 8 | const demoData = require("../demo.editable.json"); 9 | 10 | @hotComponent(module) 11 | @observer 12 | export class DemoView extends React.Component<{ model: Model }> { 13 | render() { 14 | return ( 15 |
16 |

Visualization Playground

17 | 22 |
23 | ); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /playground/src/components/EditorView.tsx: -------------------------------------------------------------------------------- 1 | import React = require("react"); 2 | import { observer } from "mobx-react"; 3 | import { hotComponent } from "../common/hotComponent"; 4 | import { PlaygroundDocumentEditor } from "./PlaygroundDocumentEditor"; 5 | import { VSCodeBrigde } from "./VSCodeBrigde"; 6 | import { Model } from "../model/Model"; 7 | 8 | @hotComponent(module) 9 | @observer 10 | export class EditorView extends React.Component<{ model: Model }> { 11 | private readonly vscodeBridge = new VSCodeBrigde(); 12 | 13 | render() { 14 | return ( 15 |
16 |

Visualization Playground Editor

17 | 22 |
23 | ); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /playground/src/components/JsonEditor.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { observer, disposeOnUnmount } from "mobx-react"; 3 | import { observable, autorun, reaction } from "mobx"; 4 | import { IRef } from "../common/utils"; 5 | import { Serializer, JsonSchemaGenerator } from "@hediet/semantic-json"; 6 | import { MonacoEditor, getLoadedMonaco } from "@hediet/monaco-editor-react"; 7 | import { modelCount } from "./modelCount"; 8 | 9 | @observer 10 | export class JsonEditor extends React.Component<{ 11 | jsonSrc: IRef; 12 | serializer: Serializer; 13 | height?: "dynamic" | "fill"; 14 | }> { 15 | private readonly editorModel = getLoadedMonaco().editor.createModel( 16 | this.props.jsonSrc.get(), 17 | "json", 18 | getLoadedMonaco().Uri.parse( 19 | `inmemory://inmemory/${modelCount.cur++}.main.json` 20 | ) 21 | ); 22 | 23 | constructor(props: any) { 24 | super(props); 25 | 26 | const s = new JsonSchemaGenerator(); 27 | 28 | getLoadedMonaco().languages.json.jsonDefaults.setDiagnosticsOptions({ 29 | validate: true, 30 | schemas: [ 31 | { 32 | uri: "https://visualization.hediet.de/schema/v0.1", 33 | fileMatch: [".main.json"], 34 | schema: s.getJsonSchemaWithDefinitions( 35 | this.props.serializer 36 | ), 37 | }, 38 | ], 39 | }); 40 | } 41 | 42 | render() { 43 | return ( 44 | 48 | ); 49 | } 50 | 51 | private isUpdating = false; 52 | 53 | @disposeOnUnmount _updateModel = reaction( 54 | () => this.props.jsonSrc.get(), 55 | val => { 56 | if (!this.isUpdating) { 57 | this.editorModel.setValue(val); 58 | } 59 | } 60 | ); 61 | 62 | componentDidMount() { 63 | this.editorModel.onDidChangeContent(() => { 64 | this.isUpdating = true; 65 | this.props.jsonSrc.set(this.editorModel.getValue()); 66 | this.isUpdating = false; 67 | }); 68 | } 69 | } 70 | 71 | @observer 72 | export class TypeScriptPreviewEditor extends React.Component<{ 73 | src: string; 74 | }> { 75 | private readonly editorModel = getLoadedMonaco().editor.createModel( 76 | this.props.src, 77 | "typescript", 78 | getLoadedMonaco().Uri.parse( 79 | `inmemory://inmemory/${modelCount.cur++}.ts` 80 | ) 81 | ); 82 | 83 | constructor(props: any) { 84 | super(props); 85 | 86 | const s = new JsonSchemaGenerator(); 87 | 88 | getLoadedMonaco().languages.typescript.typescriptDefaults.setCompilerOptions( 89 | { 90 | noLib: true, 91 | } 92 | ); 93 | getLoadedMonaco().languages.typescript.typescriptDefaults.setDiagnosticsOptions( 94 | { 95 | noSemanticValidation: true, 96 | } 97 | ); 98 | } 99 | 100 | render() { 101 | return ; 102 | } 103 | 104 | componentDidUpdate() { 105 | this.editorModel.setValue(this.props.src); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /playground/src/components/MainView.tsx: -------------------------------------------------------------------------------- 1 | import React = require("react"); 2 | import { observer } from "mobx-react"; 3 | import { hotComponent } from "../common/hotComponent"; 4 | import { Model } from "../model/Model"; 5 | import { DemoView } from "./DemoView"; 6 | import { getQueryParam } from "../model/QueryController"; 7 | import { EditorView } from "./EditorView"; 8 | import { VisualizationWithEditorView } from "./VisualizationWithEditorView"; 9 | 10 | @hotComponent(module) 11 | @observer 12 | export class MainView extends React.Component<{ model: Model }> { 13 | render() { 14 | const { model } = this.props; 15 | if (model.visualization) { 16 | return ( 17 | 21 | ); 22 | } 23 | if (getQueryParam("editor") !== null) { 24 | return ; 25 | } 26 | return ; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /playground/src/components/PlaygroundDocumentEditor.tsx: -------------------------------------------------------------------------------- 1 | import { hotComponent } from "../common/hotComponent"; 2 | import * as React from "react"; 3 | import { Model, VisualizationModel } from "../model/Model"; 4 | import { 5 | VisualizationData, 6 | VisualizationId, 7 | Theme, 8 | VisualizationFactory, 9 | globalVisualizationFactory, 10 | VisualizationView, 11 | isVisualizationData, 12 | Visualization, 13 | asVisualizationId, 14 | } from "@hediet/visualization-core"; 15 | import { observer } from "mobx-react"; 16 | import { EditableText, Checkbox, Button } from "@blueprintjs/core"; 17 | import { JsonEditor, TypeScriptPreviewEditor } from "./JsonEditor"; 18 | import { ref } from "../common/utils"; 19 | import { computed, observable, runInAction, autorun } from "mobx"; 20 | import { TypeScriptTypeGenerator } from "@hediet/semantic-json"; 21 | import { QueryBinding } from "../model/QueryController"; 22 | import { JsonDataSource } from "../model/JsonDataSource"; 23 | import { Select } from "./Select"; 24 | 25 | @hotComponent(module) 26 | @observer 27 | export class PlaygroundDocumentEditor extends React.Component<{ 28 | document: PlaygroundDocument; 29 | model: Model; 30 | editableCaptions: boolean; 31 | }> { 32 | @observable showLightTheme = false; 33 | @observable showDarkTheme = true; 34 | @observable showTypeScriptTypes = false; 35 | 36 | constructor(props: any) { 37 | super(props); 38 | 39 | new QueryBinding( 40 | "lightTheme", 41 | ref(this, "showLightTheme").map( 42 | val => (val ? ("1" as string) : undefined), 43 | val => val !== undefined 44 | ) 45 | ); 46 | new QueryBinding( 47 | "darkTheme", 48 | ref(this, "showDarkTheme").map( 49 | val => (val ? ("1" as string) : undefined), 50 | val => val !== undefined 51 | ) 52 | ); 53 | new QueryBinding( 54 | "tsTypes", 55 | ref(this, "showTypeScriptTypes").map( 56 | val => (val ? ("1" as string) : undefined), 57 | val => val !== undefined 58 | ) 59 | ); 60 | } 61 | 62 | render() { 63 | const { document, model } = this.props; 64 | return ( 65 |
66 |
67 | 72 | (this.showLightTheme = e.currentTarget.checked) 73 | } 74 | /> 75 | 80 | (this.showDarkTheme = e.currentTarget.checked) 81 | } 82 | /> 83 | 88 | (this.showTypeScriptTypes = e.currentTarget.checked) 89 | } 90 | /> 91 |
92 |
93 | {document.visualizations.map((v, idx) => ( 94 | 103 | ))} 104 |
105 |
106 | ); 107 | } 108 | } 109 | 110 | export interface PlaygroundDocument { 111 | visualizations: VisualizationInfo[]; 112 | } 113 | 114 | export interface VisualizationInfo { 115 | name: string; 116 | dataSrc: string; 117 | preferredVisualizationId: VisualizationId | undefined; 118 | } 119 | 120 | type VisualizationDataResult = 121 | | { 122 | kind: "ok"; 123 | value: VisualizationData; 124 | } 125 | | { kind: "error"; message: string }; 126 | 127 | function parseVisualizationData(json: string): VisualizationDataResult { 128 | try { 129 | const value = JSON.parse(json); 130 | if (isVisualizationData(value)) { 131 | return { kind: "ok", value }; 132 | } 133 | return { 134 | kind: "error", 135 | message: "Data is not welformed visualization data.", 136 | }; 137 | } catch (e) { 138 | return { kind: "error", message: "" + e }; 139 | } 140 | } 141 | 142 | @observer 143 | class VisualizationConfigView extends React.Component<{ 144 | config: VisualizationInfo; 145 | showLightTheme: boolean; 146 | showDarkTheme: boolean; 147 | showTypeScriptTypes: boolean; 148 | model: Model; 149 | editableCaptions: boolean; 150 | }> { 151 | @computed get data(): 152 | | { 153 | kind: "ok"; 154 | visualization: Visualization; 155 | typeScriptDeclarationSrc: string; 156 | availableVisualizations: Visualization[]; 157 | } 158 | | { 159 | kind: "error"; 160 | message: string; 161 | availableVisualizations: Visualization[]; 162 | } { 163 | const r = parseVisualizationData(this.props.config.dataSrc); 164 | 165 | if (r.kind === "error") { 166 | return Object.assign({ availableVisualizations: [] }, r); 167 | } 168 | 169 | const visualizations = globalVisualizationFactory.getVisualizations( 170 | r.value, 171 | this.props.config.preferredVisualizationId 172 | ); 173 | 174 | if (visualizations.bestVisualization) { 175 | const tsTypeGenerator = new TypeScriptTypeGenerator(); 176 | const typeSrc = tsTypeGenerator.getType( 177 | visualizations.bestVisualizationVisualizer!.serializer.asSerializer() 178 | ); 179 | const decl = tsTypeGenerator.getDefinitionSource(); 180 | return { 181 | kind: "ok", 182 | visualization: visualizations.bestVisualization, 183 | availableVisualizations: visualizations.allVisualizations, 184 | typeScriptDeclarationSrc: `type ExpectedType = ${typeSrc};\n\n${decl}`, 185 | }; 186 | } 187 | 188 | return { 189 | kind: "error", 190 | message: "No Visualization Available", 191 | availableVisualizations: visualizations.allVisualizations, 192 | }; 193 | } 194 | 195 | render() { 196 | const { 197 | config, 198 | showDarkTheme, 199 | showLightTheme, 200 | showTypeScriptTypes, 201 | editableCaptions, 202 | } = this.props; 203 | 204 | const themes = new Array(); 205 | if (showDarkTheme) { 206 | themes.push(Theme.dark); 207 | } 208 | if (showLightTheme) { 209 | themes.push(Theme.light); 210 | } 211 | 212 | return ( 213 |
214 |
215 |

216 | (config.name = e)} 220 | /> 221 |

222 |
223 | > 224 | style={{ minWidth: 100 }} 225 | options={this.data.availableVisualizations} 226 | idSelector={v => v.id.toString()} 227 | labelSelector={v => v.name} 228 | selected={ 229 | this.data.kind === "ok" 230 | ? this.data.visualization 231 | : undefined 232 | } 233 | onSelect={e => 234 | runInAction(() => { 235 | config.preferredVisualizationId = e.id; 236 | }) 237 | } 238 | /> 239 |
240 | 252 |
253 |
254 | {themes.map((theme, idx) => ( 255 |
265 | {this.data.kind === "ok" ? ( 266 | 270 | ) : ( 271 |
272 | )} 273 |
274 | ))} 275 |
284 | 288 |
289 | {showTypeScriptTypes && ( 290 |
299 | 306 |
307 | )} 308 |
309 |
310 | ); 311 | } 312 | } 313 | -------------------------------------------------------------------------------- /playground/src/components/Select.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { HTMLSelect } from "@blueprintjs/core"; 3 | 4 | export class Select extends React.Component<{ 5 | options: T[]; 6 | idSelector: (value: T) => string; 7 | labelSelector: (value: T) => string; 8 | selected: T | undefined; 9 | onSelect: (selected: T) => void; 10 | style: React.CSSProperties; 11 | }> { 12 | render() { 13 | const { 14 | idSelector, 15 | labelSelector, 16 | onSelect, 17 | options, 18 | selected, 19 | style, 20 | } = this.props; 21 | const ops = options.map(o => ({ 22 | value: idSelector(o), 23 | label: labelSelector(o), 24 | })); 25 | let value: string; 26 | if (selected === undefined) { 27 | value = "undefined"; 28 | ops.unshift({ value: "undefined", label: "(Nothing)" }); 29 | } else { 30 | value = idSelector(selected); 31 | } 32 | 33 | return ( 34 | { 39 | const selected = options.find( 40 | o => idSelector(o) === e.currentTarget.value 41 | ); 42 | if (selected) { 43 | onSelect(selected); 44 | } 45 | }} 46 | /> 47 | ); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /playground/src/components/VSCodeBrigde.tsx: -------------------------------------------------------------------------------- 1 | import { observable, autorun, runInAction, toJS, reaction } from "mobx"; 2 | import { PlaygroundDocument } from "./PlaygroundDocumentEditor"; 3 | 4 | export class VSCodeBrigde { 5 | @observable public currentDocument: PlaygroundDocument = { 6 | visualizations: [], 7 | }; 8 | private ignoreChanges = false; 9 | 10 | constructor() { 11 | window.addEventListener("message", e => { 12 | const msg = e.data as MessageFromHost; 13 | 14 | if (msg.kind === "loadContent") { 15 | this.ignoreChanges = true; 16 | runInAction("Update data", () => { 17 | this.currentDocument = msg.content as PlaygroundDocument; 18 | }); 19 | this.ignoreChanges = false; 20 | } 21 | }); 22 | 23 | reaction( 24 | () => toJS(this.currentDocument), 25 | newContent => { 26 | if (this.ignoreChanges) { 27 | return; 28 | } 29 | this.sendMessage({ 30 | kind: "onChange", 31 | newContent, 32 | }); 33 | } 34 | ); 35 | 36 | this.sendMessage({ 37 | kind: "onInit", 38 | }); 39 | } 40 | 41 | private sendMessage(m: MesssageToHost) { 42 | window.parent.postMessage(m, "*"); 43 | } 44 | } 45 | 46 | type JsonValue = unknown; 47 | 48 | type MessageFromHost = { 49 | kind: "loadContent"; 50 | content: JsonValue; 51 | }; 52 | 53 | type MesssageToHost = 54 | | { kind: "onInit" } 55 | | { kind: "log"; message: string } 56 | | { 57 | kind: "onChange"; 58 | newContent: JsonValue; 59 | }; 60 | -------------------------------------------------------------------------------- /playground/src/components/VisualizationWithEditorView.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { observer, disposeOnUnmount } from "mobx-react"; 3 | import { Model, VisualizationModel } from "../model/Model"; 4 | import classnames = require("classnames"); 5 | import { 6 | VisualizationView, 7 | Theme, 8 | Visualization, 9 | } from "@hediet/visualization-core"; 10 | import { Button, HTMLSelect, ButtonGroup } from "@blueprintjs/core"; 11 | import { observable, computed, runInAction } from "mobx"; 12 | import { QueryBinding } from "../model/QueryController"; 13 | import { ref } from "../common/utils"; 14 | import { Select } from "./Select"; 15 | 16 | @observer 17 | export class VisualizationWithEditorView extends React.Component<{ 18 | model: Model; 19 | visualizationModel: VisualizationModel; 20 | }> { 21 | @observable showEditor = true; 22 | @computed get theme(): Theme { 23 | if (this.themeId === "dark") { 24 | return Theme.dark; 25 | } 26 | return Theme.light; 27 | } 28 | 29 | @observable themeId: string | undefined; 30 | 31 | private readonly themeQueryBinding = new QueryBinding( 32 | "theme", 33 | ref(this, "themeId") 34 | ); 35 | 36 | render() { 37 | const { visualizationModel, model } = this.props; 38 | 39 | return ( 40 |
49 |
53 | 61 |
62 | 68 |
69 | (this.themeId = e.currentTarget.value)} 76 | /> 77 |
78 | 79 | > 80 | style={{ minWidth: 100 }} 81 | options={ 82 | ( 83 | visualizationModel.visualizations || { 84 | allVisualizations: [], 85 | } 86 | ).allVisualizations 87 | } 88 | idSelector={v => v.id.toString()} 89 | labelSelector={v => v.name} 90 | selected={ 91 | visualizationModel.visualizations 92 | ? visualizationModel.visualizations 93 | .bestVisualization 94 | : undefined 95 | } 96 | onSelect={e => 97 | runInAction(() => { 98 | visualizationModel.dataSource.setPreferredVisualizationId( 99 | e.id 100 | ); 101 | }) 102 | } 103 | /> 104 |
105 |
113 | {this.showEditor && ( 114 |
123 | {visualizationModel.dataSource.editorView} 124 |
125 | )} 126 |
135 | {this.renderVisualization()} 136 |
137 |
138 |
139 | ); 140 | } 141 | 142 | renderVisualization() { 143 | const { visualizationModel: model } = this.props; 144 | const visualizations = model.visualizations; 145 | 146 | if (!visualizations) { 147 | return
No/Invalid Data
; 148 | } 149 | if (!visualizations.bestVisualization) { 150 | return
Data cannot be visualized
; 151 | } 152 | return ( 153 | 157 | ); 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /playground/src/components/modelCount.ts: -------------------------------------------------------------------------------- 1 | export const modelCount = { cur: 0 }; 2 | -------------------------------------------------------------------------------- /playground/src/index.tsx: -------------------------------------------------------------------------------- 1 | (globalThis as any).MonacoEnvironment = { globalAPI: true }; 2 | 3 | import * as React from "react"; 4 | import * as ReactDOM from "react-dom"; 5 | import "./style.scss"; 6 | import { App } from "./components/App"; 7 | 8 | const elem = document.createElement("div"); 9 | elem.className = "react-root"; 10 | document.body.append(elem); 11 | ReactDOM.render(, elem); 12 | -------------------------------------------------------------------------------- /playground/src/model/JsonDataSource.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | VisualizationDataSource, 3 | VisualizationDataSourceState, 4 | } from "./VisualizationDataSource"; 5 | import { observable, computed } from "mobx"; 6 | import { 7 | VisualizationData, 8 | VisualizationId, 9 | globalVisualizationFactory, 10 | } from "@hediet/visualization-core"; 11 | import * as React from "react"; 12 | import { JsonEditor } from "../components/JsonEditor"; 13 | import { ref, Animator, Ref } from "../common/utils"; 14 | 15 | type State = { json: string; prefVisId: string | undefined }; 16 | 17 | export class JsonDataSource implements VisualizationDataSource { 18 | @observable jsonSrc: string = ""; 19 | @observable preferredVisualizationId: VisualizationId | undefined; 20 | 21 | readonly editorView = (); 22 | 23 | @observable timePassed = 0; 24 | 25 | @computed 26 | get state(): State { 27 | return { 28 | json: this.jsonSrc, 29 | prefVisId: this.preferredVisualizationId as any, 30 | }; 31 | } 32 | 33 | public setState(state: State): void { 34 | this.jsonSrc = state.json; 35 | this.preferredVisualizationId = state.prefVisId as any; 36 | } 37 | 38 | constructor(initialState?: State) { 39 | if (initialState) { 40 | this.setState(initialState); 41 | } 42 | } 43 | 44 | public setPreferredVisualizationId(id: VisualizationId | undefined): void { 45 | this.preferredVisualizationId = id; 46 | } 47 | 48 | private get normalizedData(): 49 | | { 50 | kind: "ok"; 51 | items: { 52 | data: VisualizationData; 53 | durationMs: number; 54 | preferredVisualizationId: VisualizationId | undefined; 55 | }[]; 56 | } 57 | | { kind: "error"; error: string } { 58 | try { 59 | var value = JSON.parse(this.jsonSrc); 60 | } catch (e) { 61 | return { 62 | kind: "error", 63 | error: "" + e, 64 | }; 65 | } 66 | 67 | if (!Array.isArray(value)) { 68 | value = [value]; 69 | } 70 | 71 | if (value.length === 0) { 72 | return { kind: "error", error: "bla" }; 73 | } 74 | 75 | const items = value.map((v: any) => ({ 76 | data: v, 77 | durationMs: Number(v["playground-durationMs"]) || 1500, 78 | preferredVisualizationId: v["playground-preferredVisualizationId"], 79 | })); 80 | 81 | return { kind: "ok", items }; 82 | } 83 | 84 | @computed.struct get times(): number[] { 85 | if (this.normalizedData.kind === "ok") { 86 | return this.normalizedData.items.map(i => i.durationMs); 87 | } 88 | return [1000]; 89 | } 90 | 91 | private lastAnimator: Animator | undefined; 92 | 93 | @computed get animator(): Animator { 94 | if (this.lastAnimator) { 95 | this.lastAnimator.dispose(); 96 | } 97 | return (this.lastAnimator = new Animator(this.times)); 98 | } 99 | 100 | @computed 101 | get data(): VisualizationDataSourceState { 102 | if (this.normalizedData.kind === "error") { 103 | return this.normalizedData; 104 | } 105 | 106 | return { 107 | kind: "ok", 108 | value: this.normalizedData.items[this.animator.index].data, 109 | preferredVisualizationId: this.preferredVisualizationId, 110 | }; 111 | } 112 | } 113 | 114 | class EditorView extends React.Component<{ jsonSrc: Ref }> { 115 | render() { 116 | return ( 117 | 121 | ); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /playground/src/model/Model.ts: -------------------------------------------------------------------------------- 1 | import { observable, action, computed } from "mobx"; 2 | import { 3 | Visualization, 4 | VisualizationId, 5 | globalVisualizationFactory, 6 | } from "@hediet/visualization-core"; 7 | import { Disposable } from "@hediet/std/disposable"; 8 | import { QueryBinding } from "./QueryController"; 9 | import { VisualizationDataSource } from "./VisualizationDataSource"; 10 | import { JsonDataSource } from "./JsonDataSource"; 11 | import { encodeData, decodeData } from "./lzmaCompressor"; 12 | import { MonacoBridge } from "./MonacoBridge"; 13 | import { Ref } from "../common/utils"; 14 | 15 | export class Model { 16 | public readonly dispose = Disposable.fn(); 17 | 18 | @observable 19 | public theme: "dark" | "light" = "light"; 20 | 21 | private readonly monacoBridge = new MonacoBridge(this); 22 | 23 | @observable 24 | public visualization: VisualizationModel | undefined = undefined; 25 | 26 | private readonly q = new QueryBinding( 27 | "state", 28 | new Ref( 29 | () => 30 | this.visualization 31 | ? this.visualization.dataSource.state 32 | : undefined, 33 | newState => { 34 | if (newState !== undefined) { 35 | if (!this.visualization) { 36 | this.visualization = new VisualizationModel( 37 | new JsonDataSource(newState as any) 38 | ); 39 | } else { 40 | this.visualization.dataSource.setState(newState); 41 | } 42 | } else { 43 | this.visualization = undefined; 44 | } 45 | } 46 | ).map(encodeData, decodeData) 47 | ); 48 | 49 | constructor() { 50 | const url = new URL(window.location.href); 51 | 52 | const theme = url.searchParams.get("theme"); 53 | if (theme && theme === "dark") { 54 | this.theme = "dark"; 55 | } else { 56 | this.theme = "light"; 57 | } 58 | } 59 | } 60 | 61 | export class VisualizationModel { 62 | constructor(public readonly dataSource: VisualizationDataSource) {} 63 | 64 | @computed get visualizations(): 65 | | { 66 | bestVisualization: Visualization | undefined; 67 | allVisualizations: Visualization[]; 68 | } 69 | | undefined { 70 | const data = this.dataSource.data; 71 | if (data.kind === "ok") { 72 | const vis = globalVisualizationFactory.getVisualizations( 73 | data.value, 74 | data.preferredVisualizationId 75 | ); 76 | return vis; 77 | } else { 78 | return undefined; 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /playground/src/model/MonacoBridge.ts: -------------------------------------------------------------------------------- 1 | import { Model } from "./Model"; 2 | import { Disposable } from "@hediet/std/disposable"; 3 | import { autorun } from "mobx"; 4 | import { getLoadedMonaco } from "@hediet/monaco-editor-react"; 5 | 6 | export class MonacoBridge { 7 | public readonly dispose = Disposable.fn(); 8 | 9 | constructor(private readonly model: Model) { 10 | return; 11 | this.dispose.track({ 12 | dispose: autorun(() => { 13 | getLoadedMonaco().editor.setTheme( 14 | model.theme === "light" ? "vs-light" : "vs-dark" 15 | ); 16 | }), 17 | }); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /playground/src/model/QueryController.ts: -------------------------------------------------------------------------------- 1 | import { autorun } from "mobx"; 2 | import { IRef } from "../common/utils"; 3 | 4 | export class QueryBinding { 5 | constructor( 6 | private readonly queryParamName: string, 7 | private readonly value: IRef 8 | ) { 9 | this.loadFromQuery(); 10 | autorun(() => { 11 | this.updateQuery(); 12 | }); 13 | } 14 | 15 | private loadFromQuery() { 16 | const val = getQueryParam(this.queryParamName); 17 | if (val) { 18 | this.value.set(val); 19 | } 20 | } 21 | 22 | private updateQuery() { 23 | const url = new URL(window.location.href); 24 | const val = this.value.get(); 25 | if (val !== undefined) { 26 | url.searchParams.set(this.queryParamName, val); 27 | } else { 28 | url.searchParams.delete(this.queryParamName); 29 | } 30 | history.replaceState(null, document.title, url.toString()); 31 | } 32 | } 33 | 34 | export function getQueryParam(queryParamName: string): string | null { 35 | const url = new URL(window.location.href); 36 | const val = url.searchParams.get(queryParamName); 37 | return val; 38 | } 39 | -------------------------------------------------------------------------------- /playground/src/model/VisualizationDataSource.ts: -------------------------------------------------------------------------------- 1 | import { VisualizationData, VisualizationId } from "@hediet/visualization-core"; 2 | 3 | export interface VisualizationDataSource { 4 | readonly editorView: React.ReactElement; 5 | 6 | setState(state: TState): void; 7 | setPreferredVisualizationId(id: VisualizationId | undefined): void; 8 | 9 | // is observable 10 | readonly state: TState; 11 | 12 | // is observable 13 | readonly data: VisualizationDataSourceState; 14 | } 15 | 16 | export type VisualizationDataSourceState = 17 | | { 18 | kind: "ok"; 19 | value: VisualizationData; 20 | preferredVisualizationId: VisualizationId | undefined; 21 | } 22 | | { kind: "error"; error: string }; 23 | -------------------------------------------------------------------------------- /playground/src/model/lzmaCompressor.ts: -------------------------------------------------------------------------------- 1 | import * as lzma from "lzma/src/lzma_worker"; 2 | import * as msgpack from "messagepack"; 3 | import * as base64 from "base64-js"; 4 | 5 | export function encodeData(json: unknown | undefined): string | undefined { 6 | if (json === undefined) { 7 | return undefined; 8 | } 9 | // normalize undefined 10 | json = JSON.parse(JSON.stringify(json)); 11 | const data = msgpack.encode(json); 12 | const compressed = lzma.LZMA.compress(data, 9); 13 | const compressedStr = base64.fromByteArray(compressed); 14 | 15 | return compressedStr 16 | .replace(/\+/g, "-") // Convert '+' to '-' 17 | .replace(/\//g, "_") // Convert '/' to '_' 18 | .replace(/=+$/, ""); // Remove ending '=' 19 | } 20 | 21 | export function decodeData( 22 | compressedStr: string | undefined 23 | ): unknown | undefined { 24 | if (compressedStr === undefined) { 25 | return undefined; 26 | } 27 | compressedStr += Array(5 - (compressedStr.length % 4)).join("="); 28 | compressedStr = compressedStr 29 | .replace(/\-/g, "+") // Convert '-' to '+' 30 | .replace(/\_/g, "/"); // Convert '_' to '/' 31 | 32 | const compressed2 = base64.toByteArray(compressedStr); 33 | const decompressed = lzma.LZMA.decompress(compressed2); 34 | const origData = msgpack.decode(new Uint8Array(decompressed)); 35 | return origData; 36 | } 37 | -------------------------------------------------------------------------------- /playground/src/model/types.d.ts: -------------------------------------------------------------------------------- 1 | declare module "lzma/src/lzma_worker" { 2 | const x: any; 3 | export = x; 4 | } 5 | 6 | declare module "base64-js" { 7 | const x: any; 8 | export = x; 9 | } 10 | -------------------------------------------------------------------------------- /playground/src/style.scss: -------------------------------------------------------------------------------- 1 | @import "~normalize.css"; 2 | @import "~@blueprintjs/core/lib/css/blueprint.css"; 3 | @import "~@blueprintjs/icons/lib/css/blueprint-icons.css"; 4 | @import "~@hediet/visualization-bundle/style.scss"; 5 | 6 | html, 7 | body, 8 | .react-root { 9 | height: 100%; 10 | } 11 | 12 | body { 13 | font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif; 14 | margin: 0px; 15 | padding: 0px; 16 | border: 0px; 17 | 18 | margin: 0; 19 | padding: 0; 20 | 21 | //background-color: var(--vscode-editor-background); 22 | } 23 | 24 | .component-GUI { 25 | & > .part-Header { 26 | //padding: 6px 20px 6px 0; 27 | //background-color: var(--vscode-sideBar-background); 28 | 29 | // border-top: solid 1px var(--vscode-button-background); 30 | // rgba(128, 128, 128, 0.15) 31 | } 32 | } 33 | 34 | .bp3-input, 35 | .bp3-button { 36 | border-radius: 0; 37 | } 38 | 39 | .bp3-icon { 40 | svg { 41 | color: lighten(black, 20); 42 | width: 14px; 43 | } 44 | } 45 | 46 | .component-DynamicHeightJsonEditor { 47 | .part-editor { 48 | .current-line { 49 | border: none !important; 50 | } 51 | .selectionHighlight { 52 | background: transparent !important; 53 | } 54 | } 55 | } 56 | 57 | .bp3-transition-container { 58 | z-index: 100000; 59 | } 60 | 61 | @import "./vscode-dark.scss"; 62 | @import "./vscode-light.scss"; 63 | 64 | .themeable { 65 | background-color: violet; // To debug backgrounds 66 | --visualizer-simple-text-color: var(--vscode-editor-foreground); 67 | } 68 | -------------------------------------------------------------------------------- /playground/src/visualizations.ts: -------------------------------------------------------------------------------- 1 | //// Uncomment the visualization that you work on to increase webpack build speed! 2 | //// If you create a new visualization, add it here! 3 | 4 | // import "@hediet/visualization-bundle/dist/visualizers/ast-visualizer"; /* 5 | // import "@hediet/visualization-bundle/dist/visualizers/graph/dot-graphviz-visualizer" /* 6 | // import "@hediet/visualization-bundle/dist/visualizers/graph/graph-graphviz-visualizer" /* 7 | // import "@hediet/visualization-bundle/dist/visualizers/graph/graph-visjs-visualizer" /* 8 | // import "@hediet/visualization-bundle/dist/visualizers/grid-visualizer" /* 9 | // import "@hediet/visualization-bundle/dist/visualizers/image-visualizer"; /* 10 | // import "@hediet/visualization-bundle/dist/visualizers/monaco-text-visualizer" /* 11 | // import "@hediet/visualization-bundle/dist/visualizers/monaco-text-diff-visualizer"; /* 12 | // import "@hediet/visualization-bundle/dist/visualizers/perspective-table-visualizer" /* 13 | // import "@hediet/visualization-bundle/dist/visualizers/plotly-visualizer" /* 14 | // import "@hediet/visualization-bundle/dist/visualizers/simple-text-visualizer"; /* 15 | // import "@hediet/visualization-bundle/dist/visualizers/source-visualizer"; /* 16 | // import "@hediet/visualization-bundle/dist/visualizers/svg-visualizer" /* 17 | // import "@hediet/visualization-bundle/dist/visualizers/tree-visualizer"; /* 18 | 19 | // This bundles the monaco editor. Uncomment it to load monaco dynamically. 20 | // Dynamic loading increases webpack build speed significantly. 21 | if (typeof process === undefined) { 22 | // We check for process so that we don't load monaco from nodejs. 23 | require("monaco-editor"); 24 | } 25 | 26 | // This import bundles *all* visualizations. This makes webpack super slow. 27 | import "@hediet/visualization-bundle"; // */ 28 | -------------------------------------------------------------------------------- /playground/src/vscode-light.scss: -------------------------------------------------------------------------------- 1 | .theme-light { 2 | --vscode-activityBar-background: #2c2c2c; 3 | --vscode-activityBar-dropBackground: rgba(255, 255, 255, 0.12); 4 | --vscode-activityBar-foreground: #ffffff; 5 | --vscode-activityBar-inactiveForeground: rgba(255, 255, 255, 0.6); 6 | --vscode-activityBarBadge-background: #007acc; 7 | --vscode-activityBarBadge-foreground: #ffffff; 8 | --vscode-badge-background: #c4c4c4; 9 | --vscode-badge-foreground: #333333; 10 | --vscode-breadcrumb-activeSelectionForeground: #4e4e4e; 11 | --vscode-breadcrumb-background: #ffffff; 12 | --vscode-breadcrumb-focusForeground: #4e4e4e; 13 | --vscode-breadcrumb-foreground: rgba(97, 97, 97, 0.8); 14 | --vscode-breadcrumbPicker-background: #f3f3f3; 15 | --vscode-button-background: #007acc; 16 | --vscode-button-foreground: #ffffff; 17 | --vscode-button-hoverBackground: #0062a3; 18 | --vscode-debugExceptionWidget-background: #f1dfde; 19 | --vscode-debugExceptionWidget-border: #a31515; 20 | --vscode-debugToolBar-background: #f3f3f3; 21 | --vscode-descriptionForeground: #717171; 22 | --vscode-diffEditor-insertedTextBackground: rgba(155, 185, 85, 0.2); 23 | --vscode-diffEditor-removedTextBackground: rgba(255, 0, 0, 0.2); 24 | --vscode-dropdown-background: #ffffff; 25 | --vscode-dropdown-border: #cecece; 26 | --vscode-editor-background: #ffffff; 27 | --vscode-editor-findMatchBackground: #a8ac94; 28 | --vscode-editor-findMatchHighlightBackground: rgba(234, 92, 0, 0.33); 29 | --vscode-editor-findRangeHighlightBackground: rgba(180, 180, 180, 0.3); 30 | --vscode-editor-focusedStackFrameHighlightBackground: rgba( 31 | 206, 32 | 231, 33 | 206, 34 | 0.45 35 | ); 36 | --vscode-editor-font-family: Consolas, "Courier New", monospace; 37 | --vscode-editor-font-size: 14; 38 | --vscode-editor-font-weight: normal; 39 | --vscode-editor-foreground: #000000; 40 | --vscode-editor-hoverHighlightBackground: rgba(173, 214, 255, 0.15); 41 | --vscode-editor-inactiveSelectionBackground: #e5ebf1; 42 | --vscode-editor-lineHighlightBorder: #eeeeee; 43 | --vscode-editor-rangeHighlightBackground: rgba(253, 255, 0, 0.2); 44 | --vscode-editor-selectionBackground: #add6ff; 45 | --vscode-editor-selectionHighlightBackground: rgba(173, 214, 255, 0.5); 46 | --vscode-editor-snippetFinalTabstopHighlightBorder: rgba(10, 50, 100, 0.5); 47 | --vscode-editor-snippetTabstopHighlightBackground: rgba(10, 50, 100, 0.2); 48 | --vscode-editor-stackFrameHighlightBackground: rgba(255, 255, 102, 0.45); 49 | --vscode-editor-wordHighlightBackground: rgba(87, 87, 87, 0.25); 50 | --vscode-editor-wordHighlightStrongBackground: rgba(14, 99, 156, 0.25); 51 | --vscode-editorActiveLineNumber-foreground: #0b216f; 52 | --vscode-editorBracketMatch-background: rgba(0, 100, 0, 0.1); 53 | --vscode-editorBracketMatch-border: #b9b9b9; 54 | --vscode-editorCodeLens-foreground: #999999; 55 | --vscode-editorCursor-foreground: #000000; 56 | --vscode-editorError-foreground: #e51400; 57 | --vscode-editorGroup-border: #e7e7e7; 58 | --vscode-editorGroup-dropBackground: rgba(38, 119, 203, 0.18); 59 | --vscode-editorGroupHeader-noTabsBackground: #ffffff; 60 | --vscode-editorGroupHeader-tabsBackground: #f3f3f3; 61 | --vscode-editorGutter-addedBackground: #81b88b; 62 | --vscode-editorGutter-background: #ffffff; 63 | --vscode-editorGutter-commentRangeForeground: #c5c5c5; 64 | --vscode-editorGutter-deletedBackground: #ca4b51; 65 | --vscode-editorGutter-modifiedBackground: #66afe0; 66 | --vscode-editorHint-foreground: #6c6c6c; 67 | --vscode-editorHoverWidget-background: #f3f3f3; 68 | --vscode-editorHoverWidget-border: #c8c8c8; 69 | --vscode-editorHoverWidget-statusBarBackground: #e7e7e7; 70 | --vscode-editorIndentGuide-activeBackground: #939393; 71 | --vscode-editorIndentGuide-background: #d3d3d3; 72 | --vscode-editorInfo-foreground: #008000; 73 | --vscode-editorLineNumber-activeForeground: #0b216f; 74 | --vscode-editorLineNumber-foreground: #237893; 75 | --vscode-editorLink-activeForeground: #0000ff; 76 | --vscode-editorMarkerNavigation-background: #ffffff; 77 | --vscode-editorMarkerNavigationError-background: #e51400; 78 | --vscode-editorMarkerNavigationInfo-background: #008000; 79 | --vscode-editorMarkerNavigationWarning-background: #e9a700; 80 | --vscode-editorOverviewRuler-addedForeground: rgba(0, 122, 204, 0.6); 81 | --vscode-editorOverviewRuler-border: rgba(127, 127, 127, 0.3); 82 | --vscode-editorOverviewRuler-bracketMatchForeground: #a0a0a0; 83 | --vscode-editorOverviewRuler-commonContentForeground: rgba(96, 96, 96, 0.4); 84 | --vscode-editorOverviewRuler-currentContentForeground: rgba( 85 | 64, 86 | 200, 87 | 174, 88 | 0.5 89 | ); 90 | --vscode-editorOverviewRuler-deletedForeground: rgba(0, 122, 204, 0.6); 91 | --vscode-editorOverviewRuler-errorForeground: rgba(255, 18, 18, 0.7); 92 | --vscode-editorOverviewRuler-findMatchForeground: rgba(209, 134, 22, 0.49); 93 | --vscode-editorOverviewRuler-incomingContentForeground: rgba( 94 | 64, 95 | 166, 96 | 255, 97 | 0.5 98 | ); 99 | --vscode-editorOverviewRuler-infoForeground: #008000; 100 | --vscode-editorOverviewRuler-modifiedForeground: rgba(0, 122, 204, 0.6); 101 | --vscode-editorOverviewRuler-rangeHighlightForeground: rgba( 102 | 0, 103 | 122, 104 | 204, 105 | 0.6 106 | ); 107 | --vscode-editorOverviewRuler-selectionHighlightForeground: rgba( 108 | 160, 109 | 160, 110 | 160, 111 | 0.8 112 | ); 113 | --vscode-editorOverviewRuler-warningForeground: #e9a700; 114 | --vscode-editorOverviewRuler-wordHighlightForeground: rgba( 115 | 160, 116 | 160, 117 | 160, 118 | 0.8 119 | ); 120 | --vscode-editorOverviewRuler-wordHighlightStrongForeground: rgba( 121 | 192, 122 | 160, 123 | 192, 124 | 0.8 125 | ); 126 | --vscode-editorPane-background: #ffffff; 127 | --vscode-editorRuler-foreground: #d3d3d3; 128 | --vscode-editorSuggestWidget-background: #f3f3f3; 129 | --vscode-editorSuggestWidget-border: #c8c8c8; 130 | --vscode-editorSuggestWidget-foreground: #000000; 131 | --vscode-editorSuggestWidget-highlightForeground: #0066bf; 132 | --vscode-editorSuggestWidget-selectedBackground: #d6ebff; 133 | --vscode-editorUnnecessaryCode-opacity: rgba(0, 0, 0, 0.47); 134 | --vscode-editorWarning-foreground: #e9a700; 135 | --vscode-editorWhitespace-foreground: rgba(51, 51, 51, 0.2); 136 | --vscode-editorWidget-background: #f3f3f3; 137 | --vscode-editorWidget-border: #c8c8c8; 138 | --vscode-errorForeground: #a1260d; 139 | --vscode-extensionBadge-remoteBackground: #007acc; 140 | --vscode-extensionBadge-remoteForeground: #ffffff; 141 | --vscode-extensionButton-prominentBackground: #327e36; 142 | --vscode-extensionButton-prominentForeground: #ffffff; 143 | --vscode-extensionButton-prominentHoverBackground: #28632b; 144 | --vscode-focusBorder: rgba(0, 122, 204, 0.4); 145 | --vscode-font-family: -apple-system, BlinkMacSystemFont, "Segoe WPC", 146 | "Segoe UI", "Ubuntu", "Droid Sans", ans-serif; 147 | --vscode-font-size: 13px; 148 | --vscode-font-weight: normal; 149 | --vscode-foreground: #616161; 150 | --vscode-gitDecoration-addedResourceForeground: #587c0c; 151 | --vscode-gitDecoration-conflictingResourceForeground: #6c6cc4; 152 | --vscode-gitDecoration-deletedResourceForeground: #ad0707; 153 | --vscode-gitDecoration-ignoredResourceForeground: #8e8e90; 154 | --vscode-gitDecoration-modifiedResourceForeground: #895503; 155 | --vscode-gitDecoration-submoduleResourceForeground: #1258a7; 156 | --vscode-gitDecoration-untrackedResourceForeground: #007100; 157 | --vscode-gitlens-gutterBackgroundColor: rgba(0, 0, 0, 0.05); 158 | --vscode-gitlens-gutterForegroundColor: #747474; 159 | --vscode-gitlens-gutterUncommittedForegroundColor: rgba(0, 188, 242, 0.6); 160 | --vscode-gitlens-lineHighlightBackgroundColor: rgba(0, 188, 242, 0.2); 161 | --vscode-gitlens-lineHighlightOverviewRulerColor: rgba(0, 188, 242, 0.6); 162 | --vscode-gitlens-trailingLineBackgroundColor: rgba(0, 0, 0, 0); 163 | --vscode-gitlens-trailingLineForegroundColor: rgba(153, 153, 153, 0.35); 164 | --vscode-input-background: #ffffff; 165 | --vscode-input-foreground: #616161; 166 | --vscode-input-placeholderForeground: #767676; 167 | --vscode-inputOption-activeBorder: #007acc; 168 | --vscode-inputValidation-errorBackground: #f2dede; 169 | --vscode-inputValidation-errorBorder: #be1100; 170 | --vscode-inputValidation-infoBackground: #d6ecf2; 171 | --vscode-inputValidation-infoBorder: #007acc; 172 | --vscode-inputValidation-warningBackground: #f6f5d2; 173 | --vscode-inputValidation-warningBorder: #b89500; 174 | --vscode-list-activeSelectionBackground: #0074e8; 175 | --vscode-list-activeSelectionForeground: #ffffff; 176 | --vscode-list-dropBackground: #d6ebff; 177 | --vscode-list-errorForeground: #b01011; 178 | --vscode-list-focusBackground: #d6ebff; 179 | --vscode-list-highlightForeground: #0066bf; 180 | --vscode-list-hoverBackground: #e8e8e8; 181 | --vscode-list-inactiveSelectionBackground: #e4e6f1; 182 | --vscode-list-invalidItemForeground: #b89500; 183 | --vscode-list-warningForeground: #855f00; 184 | --vscode-listFilterWidget-background: #efc1ad; 185 | --vscode-listFilterWidget-noMatchesOutline: #be1100; 186 | --vscode-listFilterWidget-outline: rgba(0, 0, 0, 0); 187 | --vscode-menu-background: #ffffff; 188 | --vscode-menu-foreground: #616161; 189 | --vscode-menu-selectionBackground: #0074e8; 190 | --vscode-menu-selectionForeground: #ffffff; 191 | --vscode-menu-separatorBackground: #888888; 192 | --vscode-menubar-selectionBackground: rgba(0, 0, 0, 0.1); 193 | --vscode-menubar-selectionForeground: #333333; 194 | --vscode-merge-commonContentBackground: rgba(96, 96, 96, 0.16); 195 | --vscode-merge-commonHeaderBackground: rgba(96, 96, 96, 0.4); 196 | --vscode-merge-currentContentBackground: rgba(64, 200, 174, 0.2); 197 | --vscode-merge-currentHeaderBackground: rgba(64, 200, 174, 0.5); 198 | --vscode-merge-incomingContentBackground: rgba(64, 166, 255, 0.2); 199 | --vscode-merge-incomingHeaderBackground: rgba(64, 166, 255, 0.5); 200 | --vscode-notificationCenterHeader-background: #e7e7e7; 201 | --vscode-notificationLink-foreground: #006ab1; 202 | --vscode-notifications-background: #f3f3f3; 203 | --vscode-notifications-border: #e7e7e7; 204 | --vscode-panel-background: #ffffff; 205 | --vscode-panel-border: rgba(128, 128, 128, 0.35); 206 | --vscode-panel-dropBackground: rgba(38, 119, 203, 0.18); 207 | --vscode-panelInput-border: #dddddd; 208 | --vscode-panelTitle-activeBorder: rgba(128, 128, 128, 0.35); 209 | --vscode-panelTitle-activeForeground: #424242; 210 | --vscode-panelTitle-inactiveForeground: rgba(66, 66, 66, 0.75); 211 | --vscode-peekView-border: #007acc; 212 | --vscode-peekViewEditor-background: #f2f8fc; 213 | --vscode-peekViewEditor-matchHighlightBackground: rgba(245, 216, 2, 0.87); 214 | --vscode-peekViewEditorGutter-background: #f2f8fc; 215 | --vscode-peekViewResult-background: #f3f3f3; 216 | --vscode-peekViewResult-fileForeground: #1e1e1e; 217 | --vscode-peekViewResult-lineForeground: #646465; 218 | --vscode-peekViewResult-matchHighlightBackground: rgba(234, 92, 0, 0.3); 219 | --vscode-peekViewResult-selectionBackground: rgba(51, 153, 255, 0.2); 220 | --vscode-peekViewResult-selectionForeground: #6c6c6c; 221 | --vscode-peekViewTitle-background: #ffffff; 222 | --vscode-peekViewTitleDescription-foreground: rgba(108, 108, 108, 0.7); 223 | --vscode-peekViewTitleLabel-foreground: #333333; 224 | --vscode-pickerGroup-border: #cccedb; 225 | --vscode-pickerGroup-foreground: #0066bf; 226 | --vscode-progressBar-background: #0e70c0; 227 | --vscode-quickInput-background: #f3f3f3; 228 | --vscode-scrollbar-shadow: #dddddd; 229 | --vscode-scrollbarSlider-activeBackground: rgba(0, 0, 0, 0.6); 230 | --vscode-scrollbarSlider-background: rgba(100, 100, 100, 0.4); 231 | --vscode-scrollbarSlider-hoverBackground: rgba(100, 100, 100, 0.7); 232 | --vscode-settings-checkboxBackground: #ffffff; 233 | --vscode-settings-checkboxBorder: #cecece; 234 | --vscode-settings-dropdownBackground: #ffffff; 235 | --vscode-settings-dropdownBorder: #cecece; 236 | --vscode-settings-dropdownListBorder: #c8c8c8; 237 | --vscode-settings-headerForeground: #444444; 238 | --vscode-settings-modifiedItemIndicator: #66afe0; 239 | --vscode-settings-numberInputBackground: #ffffff; 240 | --vscode-settings-numberInputBorder: #cecece; 241 | --vscode-settings-numberInputForeground: #616161; 242 | --vscode-settings-textInputBackground: #ffffff; 243 | --vscode-settings-textInputBorder: #cecece; 244 | --vscode-settings-textInputForeground: #616161; 245 | --vscode-sideBar-background: #f3f3f3; 246 | --vscode-sideBar-dropBackground: rgba(0, 0, 0, 0.1); 247 | --vscode-sideBarSectionHeader-background: rgba(128, 128, 128, 0.2); 248 | --vscode-sideBarTitle-foreground: #6f6f6f; 249 | --vscode-statusBar-background: #007acc; 250 | --vscode-statusBar-debuggingBackground: #cc6633; 251 | --vscode-statusBar-debuggingForeground: #ffffff; 252 | --vscode-statusBar-foreground: #ffffff; 253 | --vscode-statusBar-noFolderBackground: #68217a; 254 | --vscode-statusBar-noFolderForeground: #ffffff; 255 | --vscode-statusBarItem-activeBackground: rgba(255, 255, 255, 0.18); 256 | --vscode-statusBarItem-hoverBackground: rgba(255, 255, 255, 0.12); 257 | --vscode-statusBarItem-prominentBackground: rgba(0, 0, 0, 0.5); 258 | --vscode-statusBarItem-prominentForeground: #ffffff; 259 | --vscode-statusBarItem-prominentHoverBackground: rgba(0, 0, 0, 0.3); 260 | --vscode-statusBarItem-remoteBackground: #16825d; 261 | --vscode-statusBarItem-remoteForeground: #ffffff; 262 | --vscode-tab-activeBackground: #ffffff; 263 | --vscode-tab-activeForeground: #333333; 264 | --vscode-tab-activeModifiedBorder: #33aaee; 265 | --vscode-tab-border: #f3f3f3; 266 | --vscode-tab-inactiveBackground: #ececec; 267 | --vscode-tab-inactiveForeground: rgba(51, 51, 51, 0.7); 268 | --vscode-tab-inactiveModifiedBorder: rgba(51, 170, 238, 0.5); 269 | --vscode-tab-unfocusedActiveBackground: #ffffff; 270 | --vscode-tab-unfocusedActiveForeground: rgba(51, 51, 51, 0.7); 271 | --vscode-tab-unfocusedActiveModifiedBorder: rgba(51, 170, 238, 0.7); 272 | --vscode-tab-unfocusedInactiveForeground: rgba(51, 51, 51, 0.35); 273 | --vscode-tab-unfocusedInactiveModifiedBorder: rgba(51, 170, 238, 0.25); 274 | --vscode-terminal-ansiBlack: #000000; 275 | --vscode-terminal-ansiBlue: #0451a5; 276 | --vscode-terminal-ansiBrightBlack: #666666; 277 | --vscode-terminal-ansiBrightBlue: #0451a5; 278 | --vscode-terminal-ansiBrightCyan: #0598bc; 279 | --vscode-terminal-ansiBrightGreen: #14ce14; 280 | --vscode-terminal-ansiBrightMagenta: #bc05bc; 281 | --vscode-terminal-ansiBrightRed: #cd3131; 282 | --vscode-terminal-ansiBrightWhite: #a5a5a5; 283 | --vscode-terminal-ansiBrightYellow: #b5ba00; 284 | --vscode-terminal-ansiCyan: #0598bc; 285 | --vscode-terminal-ansiGreen: #00bc00; 286 | --vscode-terminal-ansiMagenta: #bc05bc; 287 | --vscode-terminal-ansiRed: #cd3131; 288 | --vscode-terminal-ansiWhite: #555555; 289 | --vscode-terminal-ansiYellow: #949800; 290 | --vscode-terminal-background: #ffffff; 291 | --vscode-terminal-border: rgba(128, 128, 128, 0.35); 292 | --vscode-terminal-foreground: #333333; 293 | --vscode-terminal-selectionBackground: rgba(0, 0, 0, 0.25); 294 | --vscode-textBlockQuote-background: rgba(127, 127, 127, 0.1); 295 | --vscode-textBlockQuote-border: rgba(0, 122, 204, 0.5); 296 | --vscode-textCodeBlock-background: rgba(220, 220, 220, 0.4); 297 | --vscode-textLink-activeForeground: #006ab1; 298 | --vscode-textLink-foreground: #006ab1; 299 | --vscode-textPreformat-foreground: #a31515; 300 | --vscode-textSeparator-foreground: rgba(0, 0, 0, 0.18); 301 | --vscode-titleBar-activeBackground: #dddddd; 302 | --vscode-titleBar-activeForeground: #333333; 303 | --vscode-titleBar-inactiveBackground: rgba(221, 221, 221, 0.6); 304 | --vscode-titleBar-inactiveForeground: rgba(51, 51, 51, 0.6); 305 | --vscode-tree-indentGuidesStroke: #a9a9a9; 306 | --vscode-widget-shadow: #a8a8a8; 307 | } 308 | -------------------------------------------------------------------------------- /playground/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "commonjs", 5 | "strict": true, 6 | "outDir": "dist", 7 | "skipLibCheck": true, 8 | "rootDir": "./src", 9 | "resolveJsonModule": true, 10 | "newLine": "LF", 11 | "sourceMap": true, 12 | "jsx": "react", 13 | "experimentalDecorators": true 14 | }, 15 | "include": ["src/**/*"] 16 | } 17 | -------------------------------------------------------------------------------- /playground/webpack.config.ts: -------------------------------------------------------------------------------- 1 | import * as webpack from "webpack"; 2 | import path = require("path"); 3 | import HtmlWebpackPlugin = require("html-webpack-plugin"); 4 | import MonacoWebpackPlugin = require("monaco-editor-webpack-plugin"); 5 | import ForkTsCheckerWebpackPlugin = require("fork-ts-checker-webpack-plugin"); 6 | import { CleanWebpackPlugin } from "clean-webpack-plugin"; 7 | 8 | const r = (file: string) => path.resolve(__dirname, file); 9 | 10 | module.exports = { 11 | entry: [r("src/index.tsx")], 12 | output: { 13 | path: r("dist"), 14 | filename: "[name].js", 15 | chunkFilename: "[name]-[hash].js", 16 | devtoolModuleFilenameTemplate: info => { 17 | let result = info.absoluteResourcePath.replace(/\\/g, "/"); 18 | if (!result.startsWith("file:")) { 19 | // Some paths already start with the file scheme. 20 | result = "file:///" + result; 21 | } 22 | return result; 23 | }, 24 | }, 25 | resolve: { 26 | extensions: [".webpack.js", ".web.js", ".ts", ".tsx", ".js"], 27 | }, 28 | devtool: "source-map", 29 | module: { 30 | rules: [ 31 | { 32 | test: /\.less$/, 33 | loaders: ["style-loader", "css-loader", "less-loader"], 34 | }, 35 | { test: /\.css$/, loader: "style-loader!css-loader" }, 36 | { test: /\.scss$/, loader: "style-loader!css-loader!sass-loader" }, 37 | { 38 | test: /\.(jpe?g|png|gif|eot|ttf|svg|woff|woff2|md)$/i, 39 | loader: "file-loader", 40 | }, 41 | { 42 | test: /\.tsx?$/, 43 | loader: "ts-loader", 44 | options: { transpileOnly: true }, 45 | }, 46 | { 47 | test: /\.js$/, 48 | enforce: "pre", 49 | use: ["source-map-loader"], 50 | }, 51 | ], 52 | }, 53 | node: { 54 | fs: "empty", 55 | }, 56 | plugins: (() => { 57 | const plugins: any[] = [ 58 | new HtmlWebpackPlugin({ 59 | title: "Visualization Playground", 60 | }), 61 | new ForkTsCheckerWebpackPlugin(), 62 | new CleanWebpackPlugin(), 63 | ]; 64 | 65 | plugins.push( 66 | new MonacoWebpackPlugin({ 67 | languages: ["typescript", "json"], 68 | }) 69 | ); 70 | 71 | return plugins; 72 | })(), 73 | } as webpack.Configuration; 74 | -------------------------------------------------------------------------------- /simple-demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "simple-demo", 3 | "version": "0.0.1", 4 | "license": "MIT", 5 | "scripts": { 6 | "dev": "webpack-dev-server --hot --port 8090", 7 | "build": "node --max_old_space_size=4096 --openssl-legacy-provider ../node_modules/webpack/bin/webpack.js --mode production" 8 | }, 9 | "dependencies": { 10 | "@hediet/visualization-core": "^0.1.0", 11 | "@hediet/visualization-bundle": "^0.1.0", 12 | "react": "^16.12.0", 13 | "react-dom": "^16.12.0" 14 | }, 15 | "devDependencies": { 16 | "@types/html-webpack-plugin": "^3.2.2", 17 | "@types/webpack": "^4.41.6", 18 | "@types/react": "^16.9.22", 19 | "@types/react-dom": "^16.9.5", 20 | "clean-webpack-plugin": "^3.0.0", 21 | "css-loader": "^3.4.2", 22 | "file-loader": "^5.1.0", 23 | "fork-ts-checker-webpack-plugin": "^4.0.4", 24 | "html-webpack-plugin": "^3.2.0", 25 | "monaco-editor-webpack-plugin": "^4.0.0", 26 | "raw-loader": "^4.0.0", 27 | "sass-loader": "^8.0.2", 28 | "sass": "^1.25.0", 29 | "style-loader": "^1.1.3", 30 | "ts-loader": "^6.2.1", 31 | "ts-node": "^8.6.2", 32 | "typescript": "^3.8.2", 33 | "webpack": "^4.41.6", 34 | "webpack-cli": "^3.3.11", 35 | "webpack-dev-server": "^3.10.3", 36 | "source-map-loader": "^1.0.1", 37 | "less-loader": "^6.2.0" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /simple-demo/src/index.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | VisualizationView, 3 | Theme, 4 | globalVisualizationFactory, 5 | VisualizationData, 6 | } from "@hediet/visualization-core"; 7 | 8 | // This registers all visualizations 9 | import "@hediet/visualization-bundle"; 10 | 11 | import * as React from "react"; 12 | import * as ReactDOM from "react-dom"; 13 | import "./style.scss"; 14 | 15 | class App extends React.Component { 16 | render() { 17 | const data = { 18 | kind: { graph: true as true }, 19 | nodes: [ 20 | { id: "1", label: "1" }, 21 | { id: "2", label: "2", color: "orange" }, 22 | { id: "3", label: "3" }, 23 | ], 24 | edges: [ 25 | { from: "1", to: "2", color: "red" }, 26 | { from: "1", to: "3" }, 27 | ], 28 | }; 29 | 30 | const visualizations = globalVisualizationFactory.getVisualizations( 31 | data, 32 | undefined 33 | ); 34 | 35 | return ( 36 | 40 | ); 41 | } 42 | } 43 | 44 | const elem = document.createElement("div"); 45 | elem.className = "react-root"; 46 | document.body.append(elem); 47 | ReactDOM.render(, elem); 48 | -------------------------------------------------------------------------------- /simple-demo/src/style.scss: -------------------------------------------------------------------------------- 1 | @import "~@hediet/visualization-bundle/style.scss"; 2 | 3 | html, 4 | body, 5 | .react-root { 6 | height: 100%; 7 | } 8 | -------------------------------------------------------------------------------- /simple-demo/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "commonjs", 5 | "strict": true, 6 | "outDir": "dist", 7 | "skipLibCheck": true, 8 | "rootDir": "./src", 9 | "resolveJsonModule": true, 10 | "newLine": "LF", 11 | "sourceMap": true, 12 | "jsx": "react", 13 | "experimentalDecorators": true 14 | }, 15 | "include": ["src/**/*"] 16 | } 17 | -------------------------------------------------------------------------------- /simple-demo/webpack.config.ts: -------------------------------------------------------------------------------- 1 | import * as webpack from "webpack"; 2 | import path = require("path"); 3 | import HtmlWebpackPlugin = require("html-webpack-plugin"); 4 | import MonacoWebpackPlugin = require("monaco-editor-webpack-plugin"); 5 | import ForkTsCheckerWebpackPlugin = require("fork-ts-checker-webpack-plugin"); 6 | import { CleanWebpackPlugin } from "clean-webpack-plugin"; 7 | 8 | const r = (file: string) => path.resolve(__dirname, file); 9 | 10 | module.exports = { 11 | entry: [r("src/index.tsx")], 12 | output: { 13 | path: r("dist"), 14 | filename: "[name].js", 15 | chunkFilename: "[name]-[hash].js", 16 | devtoolModuleFilenameTemplate: info => { 17 | let result = info.absoluteResourcePath.replace(/\\/g, "/"); 18 | if (!result.startsWith("file:")) { 19 | // Some paths already start with the file scheme. 20 | result = "file:///" + result; 21 | } 22 | return result; 23 | }, 24 | }, 25 | resolve: { 26 | extensions: [".webpack.js", ".web.js", ".ts", ".tsx", ".js"], 27 | }, 28 | devtool: "source-map", 29 | module: { 30 | rules: [ 31 | { 32 | test: /\.less$/, 33 | loaders: ["style-loader", "css-loader", "less-loader"], 34 | }, 35 | { test: /\.css$/, loader: "style-loader!css-loader" }, 36 | { test: /\.scss$/, loader: "style-loader!css-loader!sass-loader" }, 37 | { 38 | test: /\.(jpe?g|png|gif|eot|ttf|svg|woff|woff2|md)$/i, 39 | loader: "file-loader", 40 | }, 41 | { 42 | test: /\.tsx?$/, 43 | loader: "ts-loader", 44 | options: { transpileOnly: true }, 45 | }, 46 | { 47 | test: /\.js$/, 48 | enforce: "pre", 49 | use: ["source-map-loader"], 50 | }, 51 | ], 52 | }, 53 | node: { 54 | fs: "empty", 55 | }, 56 | plugins: (() => { 57 | const plugins: any[] = [ 58 | new HtmlWebpackPlugin({ 59 | title: "Debug Visualizer", 60 | }), 61 | new ForkTsCheckerWebpackPlugin(), 62 | new CleanWebpackPlugin(), 63 | ]; 64 | 65 | plugins.push( 66 | new MonacoWebpackPlugin({ 67 | languages: ["typescript", "json"], 68 | }) 69 | ); 70 | 71 | return plugins; 72 | })(), 73 | } as webpack.Configuration; 74 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "recommended", 3 | "rules": { 4 | "await-promise": true 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /visualization-bundle/README.md: -------------------------------------------------------------------------------- 1 | # Visualization Bundle 2 | 3 | This package registers various visualizations. If you have an idea for a cool visualization, add it here! 4 | 5 | ## Installation 6 | 7 | You can use yarn to install this library: 8 | 9 | ``` 10 | yarn add @hediet/visualization-bundle 11 | ``` 12 | 13 | Install `@hediet/visualization-core` before! 14 | 15 | ## Usage 16 | 17 | Import `@hediet/visualization-bundle` to register all default visualizations to `globalVisualizationFactory` of `@hediet/visualization-core`: 18 | 19 | ```tsx 20 | // This registers all visualizations 21 | import "@hediet/visualization-bundle"; 22 | ``` 23 | 24 | Don't forget to import the styles in your scss file: 25 | 26 | ```scss 27 | @import "~@hediet/visualization-bundle/style.scss"; 28 | ``` 29 | 30 | You will need to configure webpack 31 | 32 | - to compile scss files: 33 | 34 | ```js 35 | { test: /\.scss$/, loader: "style-loader!css-loader!sass-loader" }, 36 | ``` 37 | 38 | - to compile less files: 39 | 40 | ```js 41 | { test: /\.less$/, loaders: ["style-loader", "css-loader", "less-loader"] } 42 | ``` 43 | 44 | - to register monaco workers 45 | 46 | ```js 47 | plugins: [ 48 | // ... 49 | new MonacoWebpackPlugin({ 50 | languages: ["typescript", "json"], 51 | }), 52 | ]; 53 | ``` 54 | 55 | See docs of `@hediet/visualization-core` for how to find and render visualizations! 56 | 57 | ## Supported Visualizations 58 | 59 | You can find a playground that demonstrates all visualizations [here](https://hediet.github.io/visualization/). 60 | 61 | ### Source Visualizations 62 | 63 | - Tree Visualizer 64 | - AST Visualizer (+ Monaco Source Code View) 65 | - Grid Visualizer 66 | - SVG Visualizer 67 | - Image Visualizer 68 | - Simple Text Visualizer 69 | 70 | ### Integrated Visualization Libraries 71 | 72 | - Plotly Visualizer 73 | - Perspective JS Visualizer 74 | - VisJS Visualizer 75 | - Graphviz Graph Visualizer 76 | - Graphviz Dot Visualizer 77 | - Vis.js Visualizer 78 | - Monaco Editor Source Code Visualizer 79 | 80 | ## Setup Local Development Copy 81 | 82 | - Clone this repository. 83 | - Run `yarn` in the root folder. 84 | - Run `yarn dev` in the `visualization-bundle` folder to start tsc in watch mode. 85 | - Edit [./playground/src/visualization.ts](../playground/src/visualizations.ts) to your needs to improve webpack performance. This is highly recommended, but not required. 86 | - Run `yarn dev` in the `playground` folder to start the playground where you can debug your visualization. 87 | 88 | Any changes to the source are now reflected in the playground! 89 | 90 | ## Implementing New Visualizations 91 | 92 | First, setup your local development copy. 93 | 94 | You can use the [`simple-text-visualizer`](./src/visualizers/simple-text-visualizer/index.tsx) as starting point! 95 | Just copy the folder, rename the ids and names, export it [here](./src/visualizers/index.ts) and read through all the comments of the simple-text-visualizer! 96 | 97 | Use the "open" button in the playground to debug your new visualization! 98 | 99 | ## Machine Readable Documents 100 | 101 | - [TypeScript Declarations](https://hediet.github.io/visualization/docs/visualization-data.ts) 102 | - [JSON Schema](https://hediet.github.io/visualization/docs/visualization-data-schema.json) 103 | -------------------------------------------------------------------------------- /visualization-bundle/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@hediet/visualization-bundle", 3 | "description": "Bundles several visualizers", 4 | "version": "0.2.5", 5 | "license": "MIT", 6 | "scripts": { 7 | "dev": "tsc --watch", 8 | "build": "tsc" 9 | }, 10 | "homepage": "https://github.com/hediet/visualization", 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/hediet/visualization.git" 14 | }, 15 | "files": [ 16 | "dist", 17 | "src", 18 | "style.scss" 19 | ], 20 | "main": "./dist/index.js", 21 | "types": "./dist/index.d.ts", 22 | "dependencies": { 23 | "@finos/perspective-viewer": "^0.5.2", 24 | "@finos/perspective-viewer-d3fc": "^0.5.2", 25 | "@finos/perspective-viewer-datagrid": "^0.5.2", 26 | "@hediet/monaco-editor-react": "^0.2.0", 27 | "@hediet/semantic-json": "^0.3.15", 28 | "@hediet/std": "^0.6.0", 29 | "classnames": "^2.2.6", 30 | "line-column": "^1.0.2", 31 | "mobx": "^5.15.4", 32 | "mobx-react": "^6.1.8", 33 | "monaco-editor": "^0.25.2", 34 | "plotly.js": "^1.54.7", 35 | "react": "^16.12.0", 36 | "react-dom": "^16.12.0", 37 | "react-measure": "^2.3.0", 38 | "react-plotly.js": "^2.4.0", 39 | "react-svg-pan-zoom": "^3.8.0", 40 | "vis-data": "^7.0.0", 41 | "vis-network": "^8.0.0", 42 | "viz.js": "^2.1.2" 43 | }, 44 | "peerDependencies": { 45 | "@hediet/visualization-core": "0.*" 46 | }, 47 | "devDependencies": { 48 | "@types/classnames": "^2.2.9", 49 | "@types/line-column": "^1.0.0", 50 | "@types/plotly.js": "^1.50.16", 51 | "@types/react": "^16.9.22", 52 | "@types/react-dom": "^16.9.5", 53 | "@types/webpack": "^4.41.6", 54 | "ts-loader": "^6.2.1", 55 | "ts-node": "^8.6.2", 56 | "typescript": "^3.9.7" 57 | }, 58 | "publishConfig": { 59 | "access": "public", 60 | "registry": "https://registry.npmjs.org/" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /visualization-bundle/src/consts.ts: -------------------------------------------------------------------------------- 1 | import { namespace } from "@hediet/semantic-json/dist/src/NamespacedNamed"; 2 | 3 | export const visualizationNs = namespace("hediet.de/visualization"); 4 | -------------------------------------------------------------------------------- /visualization-bundle/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./visualizers"; 2 | -------------------------------------------------------------------------------- /visualization-bundle/src/types.d.ts: -------------------------------------------------------------------------------- 1 | declare module "react-measure" { 2 | const content: any; 3 | export default content; 4 | 5 | export type ContentRect = any; 6 | } 7 | 8 | declare module "react-plotly.js" { 9 | const content: any; 10 | export default content; 11 | } 12 | 13 | declare module "react-svg-pan-zoom" { 14 | export class Tool { } 15 | export type ReactSVGPanZoom = any; 16 | export const ReactSVGPanZoom = undefined as any; 17 | export class Value { } 18 | } 19 | -------------------------------------------------------------------------------- /visualization-bundle/src/utils/LazyLoadable.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { observer } from "mobx-react"; 3 | import { Loadable } from "./Loadable"; 4 | 5 | export function makeLazyLoadable( 6 | selector: () => Promise> 7 | ): React.ComponentClass & { preload(): Promise } { 8 | const loader = new Loadable(selector); 9 | 10 | @observer 11 | class MyComponent extends React.Component { 12 | constructor(props: any) { 13 | super(props); 14 | loader.load(); 15 | } 16 | 17 | render() { 18 | if (!loader.result) { 19 | return
Loading...
; 20 | } 21 | const C = loader.result; 22 | return ; 23 | } 24 | 25 | static async preload(): Promise { 26 | await loader.load(); 27 | } 28 | } 29 | 30 | return MyComponent; 31 | } 32 | -------------------------------------------------------------------------------- /visualization-bundle/src/utils/Loadable.tsx: -------------------------------------------------------------------------------- 1 | import { observable, runInAction } from "mobx"; 2 | 3 | export class Loadable { 4 | @observable.ref 5 | public result: T | undefined; 6 | 7 | private operation?: Promise = undefined; 8 | 9 | constructor(private readonly _load: () => Promise) {} 10 | 11 | public load(): Promise { 12 | if (!this.operation) { 13 | this.operation = (async () => { 14 | const r = await this._load(); 15 | runInAction(() => { 16 | this.result = r; 17 | }); 18 | })(); 19 | } 20 | return this.operation; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /visualization-bundle/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | import { Refinement } from "@hediet/semantic-json/dist/src/serialization/BaseSerializer"; 2 | 3 | export function withDeserializer( 4 | fromIntermediate: (val: TIntermediate) => T 5 | ): Refinement { 6 | return { 7 | canSerialize: (val: unknown): val is T => false, 8 | fromIntermediate, 9 | toIntermediate: () => { 10 | throw new Error("not supported"); 11 | }, 12 | }; 13 | } 14 | -------------------------------------------------------------------------------- /visualization-bundle/src/vars.scss: -------------------------------------------------------------------------------- 1 | .theme-dark { 2 | --visualizer-background: #263238; 3 | } 4 | .theme-light { 5 | --visualizer-background: white; 6 | } 7 | -------------------------------------------------------------------------------- /visualization-bundle/src/visualizers/ast-visualizer/AstVisualizer.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { observer, disposeOnUnmount } from "mobx-react"; 3 | import { observable, autorun, trace, computed } from "mobx"; 4 | import * as monacoTypes from "monaco-editor"; 5 | import { getLanguageId } from "../monaco-text-visualizer/getLanguageId"; 6 | import { 7 | TreeWithPathView, 8 | TreeViewModel, 9 | TreeNodeViewModel, 10 | } from "../tree-visualizer/Views"; 11 | import { sAstTree } from "."; 12 | import Measure from "react-measure"; 13 | import { MonacoEditor, getLoadedMonaco } from "@hediet/monaco-editor-react"; 14 | import { Theme } from "@hediet/visualization-core"; 15 | 16 | export interface NodeInfo { 17 | start: number; 18 | length: number; 19 | } 20 | 21 | @observer 22 | export class AstTree extends React.Component<{ 23 | model: TreeViewModel; 24 | data: typeof sAstTree.T; 25 | nodeInfoToRange: (info: NodeInfo) => monacoTypes.IRange; 26 | posToIndex: (pos: monacoTypes.IPosition) => number; 27 | theme: Theme; 28 | }> { 29 | private readonly editorRef = React.createRef(); 30 | 31 | render() { 32 | const { model, data, theme } = this.props; 33 | 34 | let languageId = "text"; 35 | if (data.fileName) { 36 | languageId = getLanguageId(data.fileName); 37 | } 38 | return ( 39 | 40 | {({ 41 | measureRef, 42 | contentRect, 43 | }: { 44 | measureRef: any; 45 | contentRect: any; 46 | }) => { 47 | return ( 48 |
1100 57 | ? "row" 58 | : "column", 59 | }} 60 | > 61 |
65 | 66 |
67 |
76 | 85 |
86 |
87 | ); 88 | }} 89 |
90 | ); 91 | } 92 | } 93 | 94 | @observer 95 | export class SourceCodeView extends React.Component<{ 96 | text: string; 97 | languageId: string; 98 | model: TreeViewModel; 99 | theme: Theme; 100 | nodeInfoToRange: (info: NodeInfo) => monacoTypes.IRange; 101 | posToIndex: (pos: monacoTypes.IPosition) => number; 102 | }> { 103 | @observable private editor: 104 | | monacoTypes.editor.IStandaloneCodeEditor 105 | | undefined; 106 | 107 | private markedDecorations: string[] = []; 108 | private selectedDecorations: string[] = []; 109 | 110 | constructor(props: any) { 111 | super(props); 112 | } 113 | 114 | layout() { 115 | if (this.editor) { 116 | this.editor.layout(); 117 | } 118 | } 119 | 120 | @computed.struct 121 | private get markedRanges(): monacoTypes.IRange[] { 122 | if (!this.editor || !this.model) { 123 | return []; 124 | } 125 | 126 | const editorModel = this.model; 127 | const ranges = this.props.model.marked.map(s => 128 | this.props.nodeInfoToRange(s.data) 129 | ); 130 | return ranges; 131 | } 132 | 133 | @disposeOnUnmount 134 | private _updateMarkedDecorations = autorun( 135 | () => { 136 | if (!this.editor) { 137 | return; 138 | } 139 | const ranges = this.markedRanges; 140 | this.markedDecorations = this.editor.deltaDecorations( 141 | this.markedDecorations, 142 | ranges.map(range => ({ 143 | range, 144 | options: { className: "marked" }, 145 | })) 146 | ); 147 | if (ranges.length > 0) { 148 | this.editor.revealRange( 149 | ranges[0], 150 | getLoadedMonaco().editor.ScrollType.Smooth 151 | ); 152 | } 153 | }, 154 | { name: "updateMarkedDecorations" } 155 | ); 156 | 157 | @computed.struct 158 | private get selected(): monacoTypes.IRange | undefined { 159 | if (this.editor && this.model) { 160 | const selected = this.props.model.selected; 161 | if (selected) { 162 | const range = this.props.nodeInfoToRange(selected.data); 163 | return range; 164 | } 165 | } 166 | return undefined; 167 | } 168 | 169 | @disposeOnUnmount 170 | private _updateSelectedDecoration = autorun( 171 | () => { 172 | console.log("_updateSelectedDecoration"); 173 | if (!this.editor) { 174 | return; 175 | } 176 | const range = this.selected; 177 | if (range) { 178 | this.selectedDecorations = this.editor.deltaDecorations( 179 | this.selectedDecorations, 180 | [ 181 | { 182 | range, 183 | options: { className: "selected" }, 184 | }, 185 | ] 186 | ); 187 | this.editor.revealRange( 188 | range, 189 | getLoadedMonaco().editor.ScrollType.Smooth 190 | ); 191 | } else { 192 | this.selectedDecorations = this.editor.deltaDecorations( 193 | this.selectedDecorations, 194 | [] 195 | ); 196 | } 197 | }, 198 | { name: "updateDecorations" } 199 | ); 200 | 201 | @computed.struct 202 | private get modelData(): { text: string; languageId: string } { 203 | const { text, languageId } = this.props; 204 | return { text, languageId }; 205 | } 206 | 207 | private lastModel: monacoTypes.editor.ITextModel | undefined; 208 | @computed get model(): monacoTypes.editor.ITextModel { 209 | const last = this.lastModel; 210 | this.lastModel = getLoadedMonaco().editor.createModel( 211 | this.modelData.text, 212 | this.modelData.languageId, 213 | undefined 214 | ); 215 | if (last) { 216 | setTimeout(() => { 217 | // we don't want to dispose the current model 218 | last.dispose(); 219 | }); 220 | } 221 | return this.lastModel; 222 | } 223 | 224 | render() { 225 | return ( 226 | { 230 | this.editor = e; 231 | e.onDidChangeCursorSelection(e => { 232 | const selectionStart = this.props.posToIndex( 233 | e.selection.getStartPosition() 234 | ); 235 | const selectionEnd = this.props.posToIndex( 236 | e.selection.getEndPosition() 237 | ); 238 | 239 | const findNode = ( 240 | m: TreeNodeViewModel 241 | ): TreeNodeViewModel | undefined => { 242 | const nodeStart = m.data.start; 243 | const nodeEnd = nodeStart + m.data.length; 244 | 245 | if ( 246 | !( 247 | nodeStart <= selectionStart && 248 | selectionEnd <= nodeEnd 249 | ) 250 | ) { 251 | return undefined; 252 | } 253 | 254 | for (const c of m.children) { 255 | const r = findNode(c); 256 | if (r) { 257 | return r; 258 | } 259 | } 260 | 261 | return m; 262 | }; 263 | 264 | if (this.props.model.root) { 265 | const n = findNode(this.props.model.root); 266 | if (n) { 267 | this.props.model.select(n); 268 | } 269 | } 270 | }); 271 | }} 272 | readOnly={true} 273 | /> 274 | ); 275 | } 276 | } 277 | -------------------------------------------------------------------------------- /visualization-bundle/src/visualizers/ast-visualizer/index.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Serializer, 3 | sLazy, 4 | sOpenObject, 5 | sArrayOf, 6 | sString, 7 | sOptionalProp, 8 | sUnion, 9 | sLiteral, 10 | sBoolean, 11 | sProp, 12 | sNumber, 13 | } from "@hediet/semantic-json"; 14 | import { visualizationNs } from "../../consts"; 15 | import { 16 | createVisualizer, 17 | globalVisualizationFactory, 18 | createReactVisualization, 19 | } from "@hediet/visualization-core"; 20 | import { createTreeViewModelFromTreeNodeData } from "../tree-visualizer"; 21 | import { AstTree, NodeInfo } from "./AstVisualizer"; 22 | import * as React from "react"; 23 | import LineColumn = require("line-column"); 24 | import * as monacoTypes from "monaco-editor"; 25 | import { getLoadedMonaco } from "@hediet/monaco-editor-react"; 26 | 27 | interface AstTreeNode { 28 | children: AstTreeNode[]; 29 | items: Item[]; 30 | segment?: string; // like "root", ".name" or "[0]" 31 | isMarked?: boolean; 32 | span: NodeInfo; 33 | } 34 | 35 | interface Item { 36 | text: string; 37 | emphasis?: "style1" | "style2" | "style3" | string; 38 | } 39 | 40 | export const sAstTreeNode: Serializer = sLazy(() => 41 | sOpenObject({ 42 | children: sArrayOf(sAstTreeNode), 43 | items: sArrayOf( 44 | sOpenObject({ 45 | text: sString(), 46 | emphasis: sOptionalProp( 47 | sUnion( 48 | [ 49 | sLiteral("style1"), 50 | sLiteral("style2"), 51 | sLiteral("style3"), 52 | sString(), 53 | ], 54 | { inclusive: true } 55 | ) 56 | ), 57 | }).defineAs(visualizationNs("AstTreeNodeItem")) 58 | ), 59 | segment: sOptionalProp(sString()), 60 | isMarked: sOptionalProp(sBoolean()), 61 | span: sOpenObject({ 62 | start: sNumber(), 63 | length: sNumber(), 64 | }), 65 | }).defineAs(visualizationNs("AstTreeNode")) 66 | ); 67 | 68 | export const sAstTree = sOpenObject({ 69 | kind: sOpenObject({ 70 | ast: sLiteral(true), 71 | tree: sLiteral(true), 72 | text: sLiteral(true), 73 | }), 74 | root: sAstTreeNode, 75 | text: sString(), 76 | fileName: sOptionalProp(sString()), 77 | }).defineAs(visualizationNs("AstTreeVisualizationData")); 78 | 79 | export const astVisualizer = createVisualizer({ 80 | id: "ast", 81 | name: "AST", 82 | serializer: sAstTree, 83 | getVisualization: (data, self) => 84 | createReactVisualization(self, { priority: 1500 }, ({ theme }) => { 85 | const m = createTreeViewModelFromTreeNodeData( 86 | data.root, 87 | node => node.span 88 | ); 89 | const l = LineColumn(data.text); 90 | function translatePosition(pos: number): monacoTypes.IPosition { 91 | let r = l.fromIndex(pos); 92 | if (!r) { 93 | r = l.fromIndex(data.text.length - 1); 94 | r.col++; 95 | } 96 | return { 97 | column: r.col, 98 | lineNumber: r.line, 99 | }; 100 | } 101 | const nodeInfoToRange = (info: NodeInfo): monacoTypes.IRange => { 102 | const start = translatePosition(info.start); 103 | const end = translatePosition(info.start + info.length); 104 | const range = getLoadedMonaco().Range.fromPositions(start, end); 105 | return range; 106 | }; 107 | function posToIndex(pos: monacoTypes.IPosition): number { 108 | const i = l.toIndex(pos.lineNumber, pos.column); 109 | if (i == -1) { 110 | return data.text.length; 111 | } 112 | return i; 113 | } 114 | 115 | return ( 116 | 123 | ); 124 | }), 125 | }); 126 | 127 | globalVisualizationFactory.addVisualizer(astVisualizer); 128 | -------------------------------------------------------------------------------- /visualization-bundle/src/visualizers/ast-visualizer/style.scss: -------------------------------------------------------------------------------- 1 | .themeable { 2 | --visualizer-ast-background: var(--visualizer-background); 3 | } 4 | .theme-dark { 5 | --visualizer-ast-marked-background: rgba(234, 92, 0, 0.33); 6 | --visualizer-ast-selected-background: #515c6a; 7 | } 8 | .theme-light { 9 | --visualizer-ast-marked-background: orange; 10 | --visualizer-ast-selected-background: yellow; 11 | } 12 | 13 | .component-AstTree { 14 | background: var(--visualizer-ast-background); 15 | 16 | .part-editor { 17 | //border-top: solid gray 1px; 18 | 19 | .marked { 20 | background: var(--visualizer-ast-marked-background); 21 | } 22 | 23 | .selected { 24 | background: var(--visualizer-ast-selected-background); 25 | opacity: 0.7; 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /visualization-bundle/src/visualizers/ast-visualizer/types.d.ts: -------------------------------------------------------------------------------- 1 | declare module "line-column" { 2 | export = function( 3 | text: string 4 | ): { 5 | fromIndex( 6 | idx: number 7 | ): { 8 | line: number; 9 | col: number; 10 | }; 11 | toIndex(line: number, column: number): number; 12 | } {}; 13 | } 14 | -------------------------------------------------------------------------------- /visualization-bundle/src/visualizers/graph/dot-graphviz-visualizer/GraphvizDotVisualizer.tsx: -------------------------------------------------------------------------------- 1 | import { observer } from "mobx-react"; 2 | import * as React from "react"; 3 | import { SvgViewer } from "../../svg-visualizer/SvgViewer"; 4 | import { Loadable } from "../../../utils/Loadable"; 5 | 6 | class VisLoader { 7 | private result: any | undefined; 8 | 9 | async getViz(): Promise { 10 | if (!this.result) { 11 | const Viz = await import("viz.js"); 12 | const { Module, render } = await import("viz.js/full.render.js"); 13 | 14 | const viz = new Viz.default({ 15 | Module: () => Module({ TOTAL_MEMORY: 1 << 30 }), 16 | render, 17 | }); 18 | 19 | this.result = viz; 20 | } 21 | return this.result; 22 | } 23 | } 24 | 25 | const vizLoader = new VisLoader(); 26 | 27 | export function getSvgFromDotCode(dotCode: string): Loadable { 28 | return new Loadable(async () => { 29 | const viz = await vizLoader.getViz(); 30 | return await viz.renderString(dotCode); 31 | }); 32 | } 33 | 34 | @observer 35 | export class GraphvizDotViewer extends React.Component<{ 36 | svgSource: Loadable; 37 | svgRef?: (element: SVGSVGElement | null) => void; 38 | }> { 39 | render() { 40 | const { svgSource } = this.props; 41 | svgSource.load(); 42 | if (!svgSource.result) { 43 | return
Loading...
; 44 | } 45 | return ( 46 | 50 | ); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /visualization-bundle/src/visualizers/graph/dot-graphviz-visualizer/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { sOpenObject, sLiteral, sString } from "@hediet/semantic-json"; 3 | import { getSvgFromDotCode, GraphvizDotViewer } from "./GraphvizDotVisualizer"; 4 | import { 5 | globalVisualizationFactory, 6 | createVisualizer, 7 | createReactVisualization, 8 | } from "@hediet/visualization-core"; 9 | import { visualizationNs } from "../../../consts"; 10 | 11 | export const graphvizDotVisualizer = createVisualizer({ 12 | id: "graphviz-dot", 13 | name: "Graphviz (Dot Data)", 14 | serializer: sOpenObject({ 15 | kind: sOpenObject({ 16 | dotGraph: sLiteral(true), 17 | }), 18 | text: sString(), 19 | }).defineAs(visualizationNs("GraphvizDotVisualizationData")), 20 | getVisualization: (data, self) => { 21 | const svgSource = getSvgFromDotCode(data.text); 22 | return createReactVisualization( 23 | self, 24 | { 25 | priority: 1500, 26 | preload: () => svgSource.load(), 27 | }, 28 | () => 29 | ); 30 | }, 31 | }); 32 | 33 | globalVisualizationFactory.addVisualizer(graphvizDotVisualizer); 34 | -------------------------------------------------------------------------------- /visualization-bundle/src/visualizers/graph/dot-graphviz-visualizer/types.d.ts: -------------------------------------------------------------------------------- 1 | declare module "viz.js" { 2 | const x: any; 3 | export default x; 4 | } 5 | 6 | declare module "viz.js/full.render.js" { 7 | export const render: any; 8 | export const Module: any; 9 | } 10 | -------------------------------------------------------------------------------- /visualization-bundle/src/visualizers/graph/graph-graphviz-visualizer/getGraphVizData.tsx: -------------------------------------------------------------------------------- 1 | import { Loadable } from "../../../utils/Loadable"; 2 | import { getSvgFromDotCode } from "../dot-graphviz-visualizer/GraphvizDotVisualizer"; 3 | 4 | export function getSvgFromGraphData( 5 | nodes: { id: string; label?: string }[], 6 | edges: { 7 | from: string; 8 | to: string; 9 | label?: string; 10 | style?: string; 11 | }[] 12 | ): Loadable { 13 | const dotContent = ` 14 | digraph MyGraph { 15 | ${nodes 16 | .map( 17 | n => 18 | `"${n.id}" [ label = ${JSON.stringify( 19 | n.label !== undefined ? n.label : n.id 20 | )} ];` 21 | ) 22 | .join("\n ")} 23 | ${edges 24 | .map( 25 | e => 26 | `"${e.from}" -> "${e.to}" [ label = ${JSON.stringify( 27 | e.label !== undefined ? e.label : "" 28 | )} style = ${JSON.stringify(e.style || "")} ];` 29 | ) 30 | .join("\n")} 31 | } 32 | `; 33 | return getSvgFromDotCode(dotContent); 34 | } 35 | -------------------------------------------------------------------------------- /visualization-bundle/src/visualizers/graph/graph-graphviz-visualizer/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { sGraph } from "../sGraph"; 3 | import { 4 | createVisualizer, 5 | globalVisualizationFactory, 6 | createReactVisualization, 7 | } from "@hediet/visualization-core"; 8 | import { GraphvizDotViewer } from "../dot-graphviz-visualizer/GraphvizDotVisualizer"; 9 | import { getSvgFromGraphData } from "./getGraphVizData"; 10 | 11 | export const graphvizGraphVisualizer = createVisualizer({ 12 | id: "graphviz-graph", 13 | name: "Graphviz", 14 | serializer: sGraph, 15 | getVisualization: (data, self) => { 16 | const svgSource = getSvgFromGraphData(data.nodes, data.edges); 17 | return createReactVisualization( 18 | self, 19 | { priority: 1000, preload: () => svgSource.load() }, 20 | () => 21 | ); 22 | }, 23 | }); 24 | 25 | globalVisualizationFactory.addVisualizer(graphvizGraphVisualizer); 26 | -------------------------------------------------------------------------------- /visualization-bundle/src/visualizers/graph/graph-visjs-visualizer/VisJsGraphViewer.tsx: -------------------------------------------------------------------------------- 1 | import { observer } from "mobx-react"; 2 | import * as React from "react"; 3 | import { DataSet, Network, Options } from "vis-network/standalone"; 4 | import { EdgeGraphData, NodeGraphData } from "../sGraph"; 5 | 6 | @observer 7 | export class VisJsGraphViewer extends React.Component<{ 8 | nodes: NodeGraphData[]; 9 | edges: EdgeGraphData[]; 10 | }> { 11 | private readonly divRef = React.createRef(); 12 | private readonly nodes = new DataSet<{ 13 | id: string; 14 | label?: string; 15 | color?: { border: string; background: string } | string; 16 | shape?: string; 17 | }>(); 18 | private readonly edges = new DataSet<{ 19 | id: string; 20 | from: string; 21 | to: string; 22 | label?: string; 23 | color?: string; 24 | dashes?: boolean | number[]; 25 | shape?: boolean; 26 | }>(); 27 | 28 | render() { 29 | return ( 30 |
35 | ); 36 | } 37 | 38 | synchronizeData() { 39 | const newNodes = new Set(); 40 | for (const n of this.props.nodes) { 41 | newNodes.add(n.id); 42 | this.nodes.update({ 43 | id: n.id, 44 | label: n.label !== undefined ? n.label : n.id, 45 | color: { background: n.color, border: n.borderColor }, 46 | shape: n.shape, 47 | }); 48 | } 49 | this.nodes.forEach((item) => { 50 | if (!newNodes.has(item.id)) { 51 | this.nodes.remove(item); 52 | } 53 | }); 54 | 55 | function getIdOfEdge(e: EdgeGraphData): string { 56 | if (e.id) { 57 | return e.id; 58 | } 59 | return e.from + "####" + e.to + "|" + e.label; 60 | } 61 | 62 | const newEdges = new Set(); 63 | for (const n of this.props.edges) { 64 | const id = getIdOfEdge(n); 65 | newEdges.add(id); 66 | this.edges.update({ 67 | id: id, 68 | label: n.label !== undefined ? n.label : "", 69 | from: n.from, 70 | to: n.to, 71 | color: n.color, 72 | dashes: { dashed: true, dotted: [1, 4], solid: false }[ 73 | n.style || "solid" 74 | ], 75 | }); 76 | } 77 | this.edges.forEach((item) => { 78 | if (!newEdges.has(item.id)) { 79 | this.edges.remove(item); 80 | } 81 | }); 82 | } 83 | 84 | componentDidUpdate() { 85 | this.synchronizeData(); 86 | } 87 | 88 | componentDidMount() { 89 | this.synchronizeData(); 90 | 91 | const data = { 92 | nodes: this.nodes, 93 | edges: this.edges, 94 | }; 95 | const options: Options = { 96 | edges: { 97 | arrows: { 98 | to: { enabled: true, scaleFactor: 1, type: "arrow" }, 99 | }, 100 | }, 101 | }; 102 | this.network = new Network(this.divRef.current!, data, options); 103 | this.divRef.current!.setAttribute("tabindex", "0"); 104 | document.addEventListener("copy", this.onCopy); 105 | } 106 | 107 | private network: Network | undefined; 108 | private readonly onCopy = (e: ClipboardEvent) => { 109 | if (!this.network) { 110 | return; 111 | } 112 | if ( 113 | !( 114 | document.activeElement && 115 | document.activeElement.className === "vis-network" 116 | ) 117 | ) { 118 | return; 119 | } 120 | 121 | const n = this.network as any; 122 | 123 | const nodesText: string[] = []; 124 | 125 | let id = 10; 126 | const visJsIdToNodeId = new Map(); 127 | 128 | // export to draw.io 129 | for (const node of Object.values(n.body.nodes) as any) { 130 | const label = node.shape.options.label; 131 | if (label === undefined) { 132 | continue; 133 | } 134 | 135 | let style = ""; 136 | if (node.shape.constructor.name === "Ellipse") { 137 | style += "ellipse;"; 138 | } else { 139 | style += "rounded=1;"; 140 | } 141 | 142 | const nodeId = id++; 143 | visJsIdToNodeId.set(node.id, nodeId); 144 | 145 | nodesText.push( 146 | ` 147 | 148 | 149 | ` 150 | ); 151 | } 152 | 153 | for (const edge of Object.values(n.body.edges) as any) { 154 | const label = edge.options.label || ""; 155 | const edgeId = id++; 156 | 157 | nodesText.push( 158 | ` 159 | 162 | 163 | ` 164 | ); 165 | } 166 | 167 | const data = ` 168 | 169 | 170 | 171 | 172 | 173 | ${nodesText.join("\n")} 174 | 175 | `; 176 | 177 | /* 178 | 179 | 180 | 181 | */ 182 | 183 | e.clipboardData!.setData("text/plain", encodeURIComponent(data)); 184 | e.preventDefault(); 185 | }; 186 | 187 | componentWillUnmount() { 188 | document.removeEventListener("copy", this.onCopy); 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /visualization-bundle/src/visualizers/graph/graph-visjs-visualizer/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { makeLazyLoadable } from "../../../utils/LazyLoadable"; 3 | import { 4 | createReactVisualization, 5 | createVisualizer, 6 | globalVisualizationFactory, 7 | } from "@hediet/visualization-core"; 8 | import { sGraph } from "../sGraph"; 9 | 10 | const VisJsGraphViewerLazyLoadable = makeLazyLoadable( 11 | async () => (await import("./VisJsGraphViewer")).VisJsGraphViewer 12 | ); 13 | 14 | export const visJsGraphVisualizer = createVisualizer({ 15 | id: "vis.js-graph", 16 | name: "vis.js", 17 | serializer: sGraph, 18 | getVisualization: (data, self) => 19 | createReactVisualization( 20 | self, 21 | { 22 | priority: 1500, 23 | preload: VisJsGraphViewerLazyLoadable.preload, 24 | }, 25 | () => ( 26 | 30 | ) 31 | ), 32 | }); 33 | 34 | globalVisualizationFactory.addVisualizer(visJsGraphVisualizer); 35 | -------------------------------------------------------------------------------- /visualization-bundle/src/visualizers/graph/graph-visjs-visualizer/style.scss: -------------------------------------------------------------------------------- 1 | .themeable { 2 | --visualizer-visjs-background: var(--visualizer-background); 3 | } 4 | 5 | .component-VisJsGraphViewer { 6 | background-color: var(--visualizer-visjs-background); 7 | } 8 | -------------------------------------------------------------------------------- /visualization-bundle/src/visualizers/graph/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./dot-graphviz-visualizer"; 2 | export * from "./graph-graphviz-visualizer"; 3 | export * from "./graph-visjs-visualizer"; 4 | -------------------------------------------------------------------------------- /visualization-bundle/src/visualizers/graph/sGraph.ts: -------------------------------------------------------------------------------- 1 | import { 2 | sArrayOf, 3 | sLiteral, 4 | sOpenObject, 5 | sOptionalProp, 6 | sString, 7 | sUnion, 8 | } from "@hediet/semantic-json"; 9 | import { visualizationNs } from "../../consts"; 10 | 11 | export type NodeGraphData = typeof sGraphNode.T; 12 | export type EdgeGraphData = typeof sGraphEdge.T; 13 | 14 | export const sGraphNode = sOpenObject({ 15 | id: sString(), 16 | label: sOptionalProp(sString(), {}), 17 | color: sOptionalProp(sString(), {}), 18 | borderColor: sOptionalProp(sString(), {}), 19 | shape: sOptionalProp(sUnion([sLiteral("ellipse"), sLiteral("box")]), {}), 20 | }).defineAs(visualizationNs("GraphNode")); 21 | 22 | export const sGraphEdge = sOpenObject({ 23 | from: sString(), 24 | to: sString(), 25 | label: sOptionalProp(sString(), {}), 26 | id: sOptionalProp(sString(), {}), 27 | color: sOptionalProp(sString(), {}), 28 | style: sOptionalProp( 29 | sUnion([sLiteral("solid"), sLiteral("dashed"), sLiteral("dotted")]), 30 | {} 31 | ), 32 | }).defineAs(visualizationNs("GraphEdge")); 33 | 34 | export const sGraph = sOpenObject({ 35 | kind: sOpenObject({ graph: sLiteral(true) }), 36 | nodes: sArrayOf(sGraphNode), 37 | edges: sArrayOf(sGraphEdge), 38 | }).defineAs(visualizationNs("GraphVisualizationData")); 39 | -------------------------------------------------------------------------------- /visualization-bundle/src/visualizers/grid-visualizer/GridVisualizer.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { observer } from "mobx-react"; 3 | import { computed, action, observable } from "mobx"; 4 | import { sGrid } from "."; 5 | 6 | export class DecoratedGridComponent extends React.Component<{ 7 | data: typeof sGrid.T; 8 | }> { 9 | render() { 10 | const d = this.props.data; 11 | 12 | const map = new Set(); 13 | function getUniqueId(tag: string): string { 14 | let n = 0; 15 | while (true) { 16 | const id = tag + (n === 0 ? "" : `-${n}`); 17 | if (!map.has(id)) { 18 | map.add(id); 19 | return id; 20 | } 21 | n++; 22 | } 23 | } 24 | 25 | const rows: GridComponent["props"]["rows"] = []; 26 | rows.push(); 27 | let rowIdx = 0; 28 | let columnCount = 0; 29 | for (const row of d.rows) { 30 | columnCount = Math.max(columnCount, row.columns.length); 31 | rows.push({ 32 | columns: row.columns.map((c, colIdx) => ({ 33 | content: ( 34 | 35 | {c.content !== undefined ? c.content : c.tag || ""} 36 | 37 | ), 38 | id: 39 | c.tag !== undefined 40 | ? getUniqueId(c.tag) 41 | : `${rowIdx}-${colIdx}`, 42 | color: c.color, 43 | kind: "data", 44 | })), 45 | }); 46 | rowIdx++; 47 | } 48 | 49 | rows.unshift({ 50 | columns: [...new Array(columnCount)].map((v, idx) => ({ 51 | id: `column-header-${idx}`, 52 | content: {idx}, 53 | kind: "header", 54 | })), 55 | }); 56 | 57 | if (d.markers) { 58 | for (const m of d.markers) { 59 | rows.push({ 60 | columns: [], 61 | }); 62 | const r = rows[rows.length - 1]; 63 | for (let i = 0; i < m.column; i++) { 64 | r.columns.push({ 65 | content: "", 66 | kind: "empty", 67 | id: `marker-spacer-${i}`, 68 | }); 69 | } 70 | 71 | r.columns.push({ 72 | id: m.id, 73 | kind: "empty", 74 | content: {m.label || m.id}, 75 | }); 76 | } 77 | } 78 | 79 | return ( 80 |
90 | 91 |
92 | ); 93 | } 94 | } 95 | 96 | function Cell(props: { children: React.ReactNode }) { 97 | return ( 98 |
106 | {props.children} 107 |
108 | ); 109 | } 110 | 111 | interface CellData { 112 | content: React.ReactNode; 113 | id: string; 114 | color?: string; 115 | kind: "data" | "header" | "empty"; 116 | } 117 | 118 | @observer 119 | class GridComponent extends React.Component<{ 120 | rows: { 121 | columns: CellData[]; 122 | }[]; 123 | markers: { 124 | id: string; 125 | 126 | row: number; 127 | column: number; 128 | rows?: number; 129 | columns?: number; 130 | 131 | label?: string; 132 | color?: string; 133 | }[]; 134 | }> { 135 | private readonly cells = new Map(); 136 | 137 | @computed get cellsGrid(): { maxWidth: number; grid: CellInfo[][] } { 138 | const unusedIds = new Set(this.cells.keys()); 139 | 140 | const rows = this.props.rows; 141 | 142 | const grid = new Array>(); 143 | 144 | let maxWidth = 0; 145 | for (let rowIdx = 0; rowIdx < rows.length; rowIdx++) { 146 | const row = rows[rowIdx]; 147 | maxWidth = Math.max(maxWidth, row.columns.length); 148 | const cellRow = new Array(); 149 | grid.push(cellRow); 150 | 151 | for (let colIdx = 0; colIdx < row.columns.length; colIdx++) { 152 | const col = row.columns[colIdx]; 153 | const key = col.id; 154 | unusedIds.delete(key); 155 | let c = this.cells.get(key); 156 | if (!c) { 157 | // New cell 158 | c = new CellInfo(col); 159 | this.cells.set(key, c); 160 | } else { 161 | c.updateCellData(col); 162 | } 163 | cellRow.push(c); 164 | } 165 | } 166 | 167 | for (const id in unusedIds.values()) { 168 | this.cells.delete(id); 169 | } 170 | 171 | return { maxWidth, grid }; 172 | } 173 | 174 | @computed 175 | private get layout(): { 176 | cells: { 177 | cell: CellInfo; 178 | top: number; 179 | left: number; 180 | width: number; 181 | height: number; 182 | }[]; 183 | width: number; 184 | height: number; 185 | } { 186 | const { maxWidth, grid } = this.cellsGrid; 187 | 188 | const maxWidths = new Array(); 189 | for (let colIdx = 0; colIdx < maxWidth; colIdx++) { 190 | let maxWidth = 30; 191 | for (const row of grid) { 192 | const col = row[colIdx]; 193 | if (col) { 194 | maxWidth = Math.max(maxWidth, col.contentWidth || 0); 195 | } 196 | } 197 | maxWidths.push(maxWidth); 198 | } 199 | 200 | const result = new Array<{ 201 | cell: CellInfo; 202 | top: number; 203 | left: number; 204 | width: number; 205 | height: number; 206 | }>(); 207 | 208 | let width = 0; 209 | let height = 0; 210 | let top = 0; 211 | let left = 0; 212 | 213 | for (const row of grid) { 214 | let maxHeight = 0; 215 | for (const col of row) { 216 | maxHeight = Math.max(maxHeight, col.contentHeight || 0); 217 | } 218 | 219 | for (let colIdx = 0; colIdx < row.length; colIdx++) { 220 | const maxWidth = maxWidths[colIdx]; 221 | const col = row[colIdx]; 222 | result.push({ 223 | cell: col, 224 | top, 225 | left, 226 | height: maxHeight, 227 | width: maxWidth, 228 | }); 229 | height = Math.max(height, top + maxHeight); 230 | width = Math.max(width, left + maxWidth); 231 | col.lastTop = col.top; 232 | col.lastLeft = col.left; 233 | col.top = top; 234 | col.left = left; 235 | 236 | col.lastContentArea = col.contentArea; 237 | col.contentArea = maxHeight * maxWidth; 238 | left += maxWidth; 239 | } 240 | 241 | left = 0; 242 | top += maxHeight; 243 | } 244 | 245 | result.sort((a, b) => a.cell.id.localeCompare(b.cell.id)); 246 | return { cells: result, height, width }; 247 | } 248 | 249 | render() { 250 | const l = this.layout; 251 | return ( 252 |
260 | {l.cells.map(i => ( 261 |
274 |
294 |
301 | {i.cell.content} 302 |
303 |
304 |
305 | ))} 306 |
307 | ); 308 | } 309 | 310 | componentDidMount() { 311 | this.updateContentSize(); 312 | } 313 | 314 | componentDidUpdate() { 315 | this.updateContentSize(); 316 | } 317 | 318 | @action 319 | updateContentSize() { 320 | for (const c of this.cells.values()) { 321 | if (c.ref) { 322 | const r = c.ref.getBoundingClientRect(); 323 | c.contentHeight = r.height + 6; 324 | c.contentWidth = r.width + 4; 325 | } 326 | } 327 | } 328 | } 329 | 330 | class CellInfo { 331 | @observable public contentWidth: number | undefined = undefined; 332 | @observable public contentHeight: number | undefined = undefined; 333 | 334 | public contentArea = 0; 335 | public lastContentArea = 0; 336 | 337 | public lastTop = 0; 338 | public top = 0; 339 | public lastLeft = 0; 340 | public left = 0; 341 | 342 | public get distance(): number { 343 | return Math.floor( 344 | Math.pow(this.top - this.lastTop, 2) + 345 | Math.pow(this.left - this.lastLeft, 2) 346 | ); 347 | } 348 | 349 | content!: React.ReactNode; 350 | 351 | ref: HTMLDivElement | null = null; 352 | 353 | public readonly id: string; 354 | public readonly kind: CellData["kind"]; 355 | 356 | constructor(cellData: CellData) { 357 | this.id = cellData.id; 358 | this.kind = cellData.kind; 359 | this.updateCellData(cellData); 360 | } 361 | 362 | public updateCellData(cellData: CellData) { 363 | this.content = cellData.content; 364 | } 365 | 366 | public readonly handleRef = (ref: HTMLDivElement | null) => { 367 | this.ref = ref; 368 | }; 369 | } 370 | -------------------------------------------------------------------------------- /visualization-bundle/src/visualizers/grid-visualizer/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { 3 | sOpenObject, 4 | sLiteral, 5 | sString, 6 | sArrayOf, 7 | sOptionalProp, 8 | sNumber, 9 | } from "@hediet/semantic-json"; 10 | import { DecoratedGridComponent } from "./GridVisualizer"; 11 | import { 12 | createVisualizer, 13 | globalVisualizationFactory, 14 | createReactVisualization, 15 | } from "@hediet/visualization-core"; 16 | import { visualizationNs } from "../../consts"; 17 | 18 | export const sGrid = sOpenObject({ 19 | kind: sOpenObject({ 20 | grid: sLiteral(true), 21 | }), 22 | columnLabels: sOptionalProp( 23 | sArrayOf( 24 | sOpenObject({ 25 | label: sOptionalProp(sString(), {}), 26 | }) 27 | ) 28 | ), 29 | rows: sArrayOf( 30 | sOpenObject({ 31 | label: sOptionalProp(sString(), {}), 32 | columns: sArrayOf( 33 | sOpenObject({ 34 | content: sOptionalProp(sString(), {}), 35 | tag: sOptionalProp(sString(), { 36 | description: 37 | "A value to identify this cell. Should be unique.", 38 | }), 39 | color: sOptionalProp(sString(), {}), 40 | }) 41 | ), 42 | }) 43 | ), 44 | markers: sOptionalProp( 45 | sArrayOf( 46 | sOpenObject({ 47 | id: sString(), 48 | 49 | row: sNumber(), 50 | column: sNumber(), 51 | rows: sOptionalProp(sNumber(), {}), 52 | columns: sOptionalProp(sNumber(), {}), 53 | 54 | label: sOptionalProp(sString(), {}), 55 | color: sOptionalProp(sString(), {}), 56 | }) 57 | ), 58 | {} 59 | ), 60 | }).defineAs(visualizationNs("GridVisualizationData")); 61 | 62 | export const gridVisualizer = createVisualizer({ 63 | id: "grid", 64 | name: "Grid", 65 | serializer: sGrid, 66 | getVisualization: (data, self) => 67 | createReactVisualization(self, { priority: 1000 }, () => ( 68 | 69 | )), 70 | }); 71 | 72 | globalVisualizationFactory.addVisualizer(gridVisualizer); 73 | -------------------------------------------------------------------------------- /visualization-bundle/src/visualizers/grid-visualizer/style.scss: -------------------------------------------------------------------------------- 1 | .themeable { 2 | --visualizer-grid-background: var(--visualizer-background); 3 | } 4 | .theme-dark { 5 | --visualizer-grid-empty-foreground: #f0f0f0; 6 | } 7 | .theme-light { 8 | --visualizer-grid-empty-foreground: #000000; 9 | } 10 | 11 | .component-GridVisualizer { 12 | background-color: var(--visualizer-grid-background); 13 | } 14 | 15 | .component-Grid { 16 | .part-data { 17 | border: 1px solid lightgray; 18 | background: #eeeeee; 19 | } 20 | 21 | .part-header { 22 | background: transparentize(white, 0.5); 23 | } 24 | 25 | .part-empty { 26 | color: var(--visualizer-grid-empty-foreground); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /visualization-bundle/src/visualizers/image-visualizer/index.tsx: -------------------------------------------------------------------------------- 1 | import { sOpenObject, sLiteral, sString, sProp } from "@hediet/semantic-json"; 2 | import * as React from "react"; 3 | import { 4 | createVisualizer, 5 | globalVisualizationFactory, 6 | createReactVisualization, 7 | } from "@hediet/visualization-core"; 8 | import { SvgViewer } from "../svg-visualizer/SvgViewer"; 9 | import { Observer, observer, disposeOnUnmount } from "mobx-react"; 10 | import { observable, autorun } from "mobx"; 11 | import { visualizationNs } from "../../consts"; 12 | 13 | export const imageVisualizer = createVisualizer({ 14 | id: "image", 15 | name: "Image", 16 | serializer: sOpenObject({ 17 | kind: sOpenObject({ 18 | imagePng: sLiteral(true), 19 | }), 20 | base64Data: sProp(sString(), { 21 | description: "The base 64 encoded PNG representation of the image", 22 | }), 23 | }).defineAs(visualizationNs("ImageVisualizationData")), 24 | getVisualization: (data, self) => 25 | createReactVisualization(self, { priority: 1000 }, () => { 26 | return ; 27 | }), 28 | }); 29 | 30 | @observer 31 | class ImageViewer extends React.Component<{ base64Data: string }> { 32 | @observable private svg: string | undefined = undefined; 33 | 34 | @disposeOnUnmount 35 | private readonly updateSvg = autorun(async () => { 36 | const src = `data:image/png;base64,${this.props.base64Data}`; 37 | 38 | const { width, height } = await new Promise<{ 39 | width: number; 40 | height: number; 41 | }>(res => { 42 | const img = new Image(); 43 | img.onload = function() { 44 | res({ width: img.width, height: img.height }); 45 | }; 46 | img.src = src; 47 | }); 48 | 49 | this.svg = ` 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | `; 62 | }); 63 | 64 | render() { 65 | if (!this.svg) { 66 | return null; 67 | } 68 | 69 | return ; 70 | } 71 | } 72 | 73 | globalVisualizationFactory.addVisualizer(imageVisualizer); 74 | -------------------------------------------------------------------------------- /visualization-bundle/src/visualizers/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./ast-visualizer"; 2 | export * from "./graph"; 3 | export * from "./grid-visualizer"; 4 | export * from "./image-visualizer"; 5 | export * from "./monaco-text-visualizer"; 6 | export * from "./monaco-text-diff-visualizer"; 7 | export * from "./perspective-table-visualizer"; 8 | export * from "./plotly-visualizer"; 9 | export * from "./simple-text-visualizer"; 10 | export * from "./source-visualizer"; 11 | export * from "./svg-visualizer"; 12 | export * from "./tree-visualizer"; 13 | -------------------------------------------------------------------------------- /visualization-bundle/src/visualizers/monaco-text-diff-visualizer/MonacoEditor.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { observer, disposeOnUnmount } from "mobx-react"; 3 | import { observable, autorun } from "mobx"; 4 | import * as monacoTypes from "monaco-editor"; 5 | import { Theme } from "@hediet/visualization-core"; 6 | import { getLoadedMonaco } from "@hediet/monaco-editor-react"; 7 | 8 | export function getLanguageId(fileName: string): string { 9 | const l = getLoadedMonaco().languages.getLanguages(); 10 | const result = l.find((l) => { 11 | if (l.filenamePatterns) { 12 | for (const p of l.filenamePatterns) { 13 | if (new RegExp(p).test(fileName)) { 14 | return true; 15 | } 16 | } 17 | } 18 | if (l.extensions) { 19 | for (const p of l.extensions) { 20 | if (fileName.endsWith(p)) { 21 | return true; 22 | } 23 | } 24 | } 25 | 26 | return false; 27 | }); 28 | 29 | if (result) { 30 | return result.id; 31 | } 32 | return "text"; 33 | } 34 | 35 | @observer 36 | export class MonacoDiffEditor extends React.Component<{ 37 | originalText: string; 38 | modifiedText: string; 39 | fileName?: string; 40 | theme: Theme; 41 | }> { 42 | @observable private editor: 43 | | monacoTypes.editor.IStandaloneDiffEditor 44 | | undefined; 45 | 46 | componentWillUnmount() { 47 | if (this.editor) { 48 | this.editor.dispose(); 49 | } 50 | } 51 | 52 | get languageId(): string { 53 | if (!this.props.fileName) { 54 | return "text"; 55 | } 56 | return getLanguageId(this.props.fileName); 57 | } 58 | 59 | private originalModel: monacoTypes.editor.ITextModel | undefined = 60 | undefined; 61 | private modifiedModel: monacoTypes.editor.ITextModel | undefined = 62 | undefined; 63 | 64 | @disposeOnUnmount 65 | private _updateText = autorun(() => { 66 | if (this.editor) { 67 | const originalModel = getLoadedMonaco().editor.createModel( 68 | this.props.originalText, 69 | this.languageId, 70 | undefined 71 | ); 72 | const modifiedModel = getLoadedMonaco().editor.createModel( 73 | this.props.modifiedText, 74 | this.languageId, 75 | undefined 76 | ); 77 | 78 | this.editor.setModel({ 79 | original: originalModel, 80 | modified: modifiedModel, 81 | }); 82 | 83 | if (this.originalModel) { 84 | this.originalModel.dispose(); 85 | } 86 | this.originalModel = originalModel; 87 | 88 | if (this.modifiedModel) { 89 | this.modifiedModel.dispose(); 90 | } 91 | this.modifiedModel = modifiedModel; 92 | } 93 | }); 94 | 95 | private readonly setEditorDiv = (editorDiv: HTMLDivElement) => { 96 | if (!editorDiv) { 97 | return; 98 | } 99 | this.editor = getLoadedMonaco().editor.createDiffEditor(editorDiv, { 100 | automaticLayout: true, 101 | scrollBeyondLastLine: false, 102 | minimap: { enabled: false }, 103 | fixedOverflowWidgets: true, 104 | readOnly: true, 105 | theme: this.props.theme.kind === "dark" ? "vs-dark" : "vs", 106 | renderWhitespace: "all", 107 | ignoreTrimWhitespace: false, 108 | }); 109 | if (this.originalModel && this.modifiedModel) { 110 | this.editor.setModel({ 111 | original: this.originalModel!, 112 | modified: this.modifiedModel!, 113 | }); 114 | } 115 | }; 116 | 117 | render() { 118 | return ( 119 |
120 |
121 |
122 | ); 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /visualization-bundle/src/visualizers/monaco-text-diff-visualizer/index.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | sOpenObject, 3 | sLiteral, 4 | sString, 5 | sOptionalProp, 6 | sProp, 7 | } from "@hediet/semantic-json"; 8 | import * as React from "react"; 9 | import { 10 | createReactVisualization, 11 | createVisualizer, 12 | globalVisualizationFactory, 13 | } from "@hediet/visualization-core"; 14 | import { visualizationNs } from "../../consts"; 15 | import { makeLazyLoadable } from "../../utils/LazyLoadable"; 16 | 17 | const MonacoDiffEditorLazyLoadable = makeLazyLoadable( 18 | async () => (await import("./MonacoEditor")).MonacoDiffEditor 19 | ); 20 | 21 | export const monacoTextDiffVisualizer = createVisualizer({ 22 | id: "monaco-text-diff", 23 | name: "Text Diff", 24 | serializer: sOpenObject({ 25 | kind: sOpenObject({ 26 | text: sLiteral(true), 27 | }), 28 | text: sProp(sString(), { description: "The text to show" }), 29 | otherText: sProp(sString(), { 30 | description: "The text to compare against", 31 | }), 32 | fileName: sOptionalProp(sString(), { 33 | description: 34 | "An optional filename that might be used for chosing a syntax highlighter", 35 | }), 36 | }).defineAs(visualizationNs("MonacoTextDiffVisualizationData")), 37 | getVisualization: (data, self) => 38 | createReactVisualization( 39 | self, 40 | { priority: 900, preload: MonacoDiffEditorLazyLoadable.preload }, 41 | ({ theme }) => ( 42 | 48 | ) 49 | ), 50 | }); 51 | 52 | globalVisualizationFactory.addVisualizer(monacoTextDiffVisualizer); 53 | -------------------------------------------------------------------------------- /visualization-bundle/src/visualizers/monaco-text-diff-visualizer/style.scss: -------------------------------------------------------------------------------- 1 | .themeable { 2 | --visualizer-monaco-editor-diff-visualizer-background: var( 3 | --visualizer-background 4 | ); 5 | } 6 | 7 | .component-monaco-editor-diff-visualizer { 8 | background-color: var( 9 | --visualizer-monaco-editor-diff-visualizer-background 10 | ); 11 | width: 100%; 12 | height: 100%; 13 | min-width: 0; 14 | box-sizing: border-box; 15 | display: flex; 16 | padding: 2px; 17 | border: 0.5px solid gray; 18 | 19 | .part-editor { 20 | min-width: 0; 21 | min-height: 0; 22 | flex: 1; 23 | 24 | .current-line { 25 | border: none !important; 26 | } 27 | .selectionHighlight { 28 | background: transparent !important; 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /visualization-bundle/src/visualizers/monaco-text-visualizer/MonacoEditor.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { observer, disposeOnUnmount } from "mobx-react"; 3 | import { observable, autorun } from "mobx"; 4 | import * as monacoTypes from "monaco-editor"; 5 | import { Theme } from "@hediet/visualization-core"; 6 | import { getLoadedMonaco } from "@hediet/monaco-editor-react"; 7 | import { getLanguageId } from "./getLanguageId"; 8 | 9 | export interface Decoration { 10 | label?: string; 11 | range: monacoTypes.IRange; 12 | } 13 | 14 | @observer 15 | export class MonacoEditor extends React.Component<{ 16 | text: string; 17 | theme: Theme; 18 | fileName?: string; 19 | decorations?: Decoration[]; 20 | jsonSchemas?: { schema: unknown }[]; 21 | }> { 22 | public static nextId = 0; 23 | 24 | @observable private editor: 25 | | monacoTypes.editor.IStandaloneCodeEditor 26 | | undefined; 27 | 28 | componentWillUnmount() { 29 | if (this.editor) { 30 | this.editor.dispose(); 31 | } 32 | } 33 | 34 | get languageId(): string { 35 | if (!this.props.fileName) { 36 | return "text"; 37 | } 38 | return getLanguageId(this.props.fileName); 39 | } 40 | 41 | private model: monacoTypes.editor.ITextModel | undefined = undefined; 42 | 43 | private prevDecorations: string[] = []; 44 | 45 | @disposeOnUnmount 46 | private _updateText = autorun(() => { 47 | if (this.editor) { 48 | const decorations = 49 | new Array(); 50 | const propDecorations = this.props.decorations || []; 51 | const lines = this.props.text.split("\n"); 52 | const mappedLines = lines.map((l, idx) => { 53 | if (idx === lines.length - 1) { 54 | return l; 55 | } 56 | 57 | if ( 58 | !propDecorations.some( 59 | (d) => 60 | ((d.range.endLineNumber === idx + 2 && 61 | d.range.endColumn === 1) || 62 | (d.range.startLineNumber === idx + 1 && 63 | d.range.startColumn >= l.length + 1)) && 64 | (d.range.startColumn !== d.range.endColumn || 65 | d.range.startLineNumber !== 66 | d.range.endLineNumber) 67 | ) 68 | ) { 69 | return l; 70 | } 71 | 72 | decorations.push({ 73 | range: { 74 | startLineNumber: idx + 1, 75 | endLineNumber: idx + 1, 76 | startColumn: l.length + 1, 77 | endColumn: l.length + 2, 78 | }, 79 | options: { 80 | inlineClassName: "decoration-whitespace", 81 | }, 82 | }); 83 | return l + "␊"; // ␍␊ 84 | }); 85 | const text = mappedLines.join("\n"); 86 | 87 | const modelId = MonacoEditor.nextId++; 88 | 89 | const model = getLoadedMonaco().editor.createModel( 90 | text, 91 | this.languageId, 92 | getLoadedMonaco().Uri.parse( 93 | `inmemory://inmemory/model${modelId}` 94 | ) 95 | ); 96 | 97 | this.editor.setModel(model); 98 | 99 | if (this.props.jsonSchemas) { 100 | const existingSchemas = 101 | getLoadedMonaco().languages.json.jsonDefaults 102 | .diagnosticsOptions.schemas || []; 103 | 104 | getLoadedMonaco().languages.json.jsonDefaults.setDiagnosticsOptions( 105 | { 106 | validate: true, 107 | schemas: [ 108 | ...existingSchemas.filter( 109 | (s) => 110 | !s.uri.startsWith( 111 | "https://example.org/schemas/temp/" 112 | ) 113 | ), 114 | ...this.props.jsonSchemas.map((s, idx) => ({ 115 | uri: "https://example.org/schemas/temp/" + idx, 116 | fileMatch: [`model${modelId}`], 117 | schema: s.schema, 118 | })), 119 | ], 120 | } 121 | ); 122 | } 123 | 124 | this.prevDecorations = this.editor.deltaDecorations( 125 | [], 126 | [ 127 | ...decorations, 128 | ...propDecorations.map((d) => { 129 | const r = getLoadedMonaco().Range.lift(d.range); 130 | return { 131 | range: r, 132 | options: { 133 | hoverMessage: d.label 134 | ? { value: d.label, isTrusted: false } 135 | : undefined, 136 | className: r.isEmpty() 137 | ? "decoration-empty" 138 | : "decoration", 139 | }, 140 | }; 141 | }), 142 | ] 143 | ); 144 | 145 | /* 146 | this.editor!.updateOptions({ readOnly: false }); 147 | 148 | setTimeout(() => { 149 | const a = this.editor!.getAction( 150 | "editor.action.formatDocument" 151 | ); 152 | a.run().then(() => { 153 | this.editor!.updateOptions({ readOnly: true }); 154 | }); 155 | }, 200);*/ 156 | 157 | if (this.model) { 158 | this.model.dispose(); 159 | } 160 | this.model = model; 161 | } 162 | }); 163 | 164 | private readonly setEditorDiv = (editorDiv: HTMLDivElement) => { 165 | if (!editorDiv) { 166 | return; 167 | } 168 | this.editor = getLoadedMonaco().editor.create(editorDiv, { 169 | model: null, 170 | automaticLayout: true, 171 | scrollBeyondLastLine: false, 172 | minimap: { enabled: false }, 173 | fixedOverflowWidgets: true, 174 | readOnly: true, 175 | theme: this.props.theme.kind === "dark" ? "vs-dark" : "vs", 176 | renderWhitespace: "all", 177 | renderValidationDecorations: "on", 178 | }); 179 | }; 180 | 181 | render() { 182 | return ( 183 |
184 |
185 |
186 | ); 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /visualization-bundle/src/visualizers/monaco-text-visualizer/getLanguageId.tsx: -------------------------------------------------------------------------------- 1 | import { getLoadedMonaco } from "@hediet/monaco-editor-react"; 2 | 3 | export function getLanguageId(fileName: string): string { 4 | const l = getLoadedMonaco().languages.getLanguages(); 5 | const result = l.find(l => { 6 | if (l.filenamePatterns) { 7 | for (const p of l.filenamePatterns) { 8 | if (new RegExp(p).test(fileName)) { 9 | return true; 10 | } 11 | } 12 | } 13 | if (l.extensions) { 14 | for (const p of l.extensions) { 15 | if (fileName.endsWith(p)) { 16 | return true; 17 | } 18 | } 19 | } 20 | 21 | return false; 22 | }); 23 | 24 | if (result) { 25 | return result.id; 26 | } 27 | return "text"; 28 | } 29 | -------------------------------------------------------------------------------- /visualization-bundle/src/visualizers/monaco-text-visualizer/index.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | sOpenObject, 3 | sLiteral, 4 | sString, 5 | sOptionalProp, 6 | sProp, 7 | sArrayOf, 8 | sNumber, 9 | sUnion, 10 | sAny, 11 | } from "@hediet/semantic-json"; 12 | import * as React from "react"; 13 | import { 14 | createVisualizer, 15 | globalVisualizationFactory, 16 | createReactVisualization, 17 | } from "@hediet/visualization-core"; 18 | import { visualizationNs } from "../../consts"; 19 | import { makeLazyLoadable } from "../../utils/LazyLoadable"; 20 | import { Decoration } from "./MonacoEditor"; 21 | 22 | export const MonacoEditorLazyLoadable = makeLazyLoadable( 23 | async () => (await import("./MonacoEditor")).MonacoEditor 24 | ); 25 | 26 | export const LineColumnPosition = sOpenObject({ 27 | line: sProp(sNumber(), { description: "The 0-based line number" }), 28 | column: sProp(sNumber(), { description: "The 0-based column number" }), 29 | }).defineAs(visualizationNs("LineColumnPosition")); 30 | 31 | export const LineColumnRange = sOpenObject({ 32 | start: sProp(LineColumnPosition, { description: "The start position" }), 33 | end: sProp(LineColumnPosition, { description: "The end position" }), 34 | }).defineAs(visualizationNs("LineColumnRange")); 35 | 36 | export const OffsetPosition = sNumber().defineAs( 37 | visualizationNs("OffsetPosition") 38 | ); 39 | 40 | export const OffsetRange = sOpenObject({ 41 | start: sProp(OffsetPosition, { description: "The start position" }), 42 | end: sProp(OffsetPosition, { description: "The end position" }), 43 | }).defineAs(visualizationNs("OffsetRange")); 44 | 45 | export const monacoTextVisualizer = createVisualizer({ 46 | id: "monaco-text", 47 | name: "Text", 48 | serializer: sOpenObject({ 49 | kind: sOpenObject({ 50 | text: sLiteral(true), 51 | }), 52 | text: sProp(sString(), { description: "The text to show" }), 53 | decorations: sOptionalProp( 54 | sArrayOf( 55 | sOpenObject({ 56 | range: sUnion([LineColumnRange]), 57 | label: sOptionalProp(sString()), 58 | }) 59 | ) 60 | ), 61 | jsonSchemas: sOptionalProp( 62 | sArrayOf( 63 | sOpenObject({ 64 | schema: sProp(sAny(), { 65 | description: 66 | "A json schema object that is applied when the fileName indicates a JSON document.", 67 | }), 68 | }) 69 | ), 70 | { 71 | description: 72 | "An array of schemas used to validate JSON documents.", 73 | } 74 | ), 75 | fileName: sOptionalProp(sString(), { 76 | description: 77 | "An optional filename that might be used for chosing a syntax highlighter", 78 | }), 79 | }).defineAs(visualizationNs("MonacoTextVisualizationData")), 80 | getVisualization: (data, self) => 81 | createReactVisualization( 82 | self, 83 | { priority: 500, preload: MonacoEditorLazyLoadable.preload }, 84 | function ({ theme }) { 85 | const decorations = data.decorations 86 | ? data.decorations.map((d) => ({ 87 | label: d.label, 88 | range: { 89 | startLineNumber: d.range.start.line + 1, 90 | startColumn: d.range.start.column + 1, 91 | endLineNumber: d.range.end.line + 1, 92 | endColumn: d.range.end.column + 1, 93 | }, 94 | })) 95 | : []; 96 | 97 | return ( 98 | 105 | ); 106 | } 107 | ), 108 | }); 109 | 110 | globalVisualizationFactory.addVisualizer(monacoTextVisualizer); 111 | -------------------------------------------------------------------------------- /visualization-bundle/src/visualizers/monaco-text-visualizer/style.scss: -------------------------------------------------------------------------------- 1 | .themeable { 2 | --visualizer-monaco-editor-background: var(--visualizer-background); 3 | } 4 | 5 | .component-monaco-editor { 6 | background-color: var(--visualizer-monaco-editor-background); 7 | width: 100%; 8 | height: 100%; 9 | min-width: 0; 10 | box-sizing: border-box; 11 | display: flex; 12 | padding: 2px; 13 | border: 0.5px solid gray; 14 | 15 | .part-editor { 16 | min-width: 0; 17 | min-height: 0; 18 | flex: 1; 19 | 20 | .current-line { 21 | border: none !important; 22 | } 23 | .selectionHighlight { 24 | background: transparent !important; 25 | } 26 | } 27 | } 28 | 29 | .decoration-whitespace { 30 | color: black !important; 31 | opacity: 0.4; 32 | } 33 | 34 | .decoration-empty { 35 | margin-left: -1px; 36 | border-left: solid rgba(255, 0, 0, 0.815) 3px; 37 | } 38 | 39 | .decoration { 40 | background-color: rgba(250, 36, 36, 0.459); 41 | } 42 | -------------------------------------------------------------------------------- /visualization-bundle/src/visualizers/perspective-table-visualizer/PerspectiveDataViewer.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Table } from "@finos/perspective"; 3 | import "@finos/perspective-viewer"; 4 | import "@finos/perspective-viewer-datagrid"; 5 | import "@finos/perspective-viewer-d3fc"; 6 | import "@finos/perspective-viewer/themes/all-themes.css"; 7 | import { HTMLPerspectiveViewerElement } from "@finos/perspective-viewer"; 8 | import { observer } from "mobx-react"; 9 | import { computed, observable } from "mobx"; 10 | 11 | @observer 12 | export class PerspectiveDataViewer extends React.Component< 13 | { data: object[] } & React.HTMLAttributes 14 | > { 15 | private readonly nodeRef = React.createRef(); 16 | 17 | @observable key = 0; 18 | 19 | private oldSchemaStr: string | undefined; 20 | 21 | private async replaceData() { 22 | const elem = this.nodeRef.current!; 23 | const data = this.props.data; 24 | const table = (elem as any).worker.table(data) as Table; 25 | 26 | const newSchema = await table.schema(); 27 | const newSchemaStr = JSON.stringify(newSchema); 28 | const oldSchemaStr = this.oldSchemaStr; 29 | this.oldSchemaStr = newSchemaStr; 30 | 31 | if (oldSchemaStr && oldSchemaStr !== newSchemaStr) { 32 | // perspective js does not like it if new data arrives that has a different schema. 33 | // We check if the schema changed an remount the component if it did. 34 | this.key++; 35 | } else { 36 | await elem.load(data); 37 | } 38 | 39 | if (!(elem as any)._show_config) { 40 | elem.toggleConfig(); 41 | } 42 | } 43 | 44 | componentDidUpdate() { 45 | this.replaceData(); 46 | } 47 | 48 | componentDidMount() { 49 | this.replaceData(); 50 | } 51 | 52 | render() { 53 | return ( 54 | 60 | ); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /visualization-bundle/src/visualizers/perspective-table-visualizer/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { sOpenObject, sLiteral, sArrayOf, sProp } from "@hediet/semantic-json"; 3 | import { toJS } from "mobx"; 4 | import { 5 | createVisualizer, 6 | globalVisualizationFactory, 7 | createReactVisualization, 8 | } from "@hediet/visualization-core"; 9 | import { makeLazyLoadable } from "../../utils/LazyLoadable"; 10 | import { visualizationNs } from "../../consts"; 11 | 12 | export const sTable = sOpenObject({ 13 | kind: sOpenObject({ 14 | table: sLiteral(true), 15 | }), 16 | rows: sProp(sArrayOf(sOpenObject({})), { 17 | description: 18 | "An array of objects. The properties of the objects are used as columns.", 19 | }), 20 | }).defineAs(visualizationNs("TableVisualizationData")); 21 | 22 | const PerspectiveDataViewerLazyLoadable = makeLazyLoadable( 23 | async () => (await import("./PerspectiveDataViewer")).PerspectiveDataViewer 24 | ); 25 | 26 | export const perspectiveTableVisualizer = createVisualizer({ 27 | id: "perspective-table", 28 | name: "Perspective Table", 29 | serializer: sTable, 30 | getVisualization: (data, self) => 31 | createReactVisualization( 32 | self, 33 | { 34 | priority: 1000, 35 | preload: PerspectiveDataViewerLazyLoadable.preload, 36 | }, 37 | ({ theme }) => ( 38 | 51 | ) 52 | ), 53 | }); 54 | 55 | globalVisualizationFactory.addVisualizer(perspectiveTableVisualizer); 56 | -------------------------------------------------------------------------------- /visualization-bundle/src/visualizers/plotly-visualizer/PlotlyViewer.tsx: -------------------------------------------------------------------------------- 1 | import { Theme } from "@hediet/visualization-core"; 2 | import { observer } from "mobx-react"; 3 | import * as React from "react"; 4 | import Plot from "react-plotly.js"; 5 | 6 | // needs to be overwriten since the table layout is not defined in @types/plotly.js 7 | interface PlotlyLayoutAnyTemplate extends Plotly.Layout { 8 | template: any; 9 | } 10 | 11 | @observer 12 | export class PlotlyViewer extends React.Component<{ 13 | data: Plotly.Data[]; 14 | layout?: Partial; 15 | theme: Theme; 16 | onReady: () => void; 17 | }> { 18 | render() { 19 | const { theme, data, layout } = this.props; 20 | const computedLayout = Object.assign( 21 | {}, 22 | theme.id === "dark" ? getLayout(theme) : {}, 23 | layout || {} 24 | ); 25 | 26 | if ( 27 | data.length === 1 && 28 | data[0].type === "mesh3d" && 29 | (layout || {}).margin === undefined 30 | ) { 31 | // Fixes https://github.com/JetBrains/rider-debug-visualizer-web-view/issues/6 32 | Object.assign(computedLayout, { 33 | margin: { 34 | l: 0, 35 | r: 0, 36 | b: 0, 37 | t: 0, 38 | pad: 0, 39 | }, 40 | }); 41 | } 42 | 43 | return ( 44 | 51 | ); 52 | } 53 | } 54 | 55 | function getLayout(theme: Theme): Partial { 56 | return { 57 | colorway: [ 58 | "#636efa", 59 | "#EF553B", 60 | "#00cc96", 61 | "#ab63fa", 62 | "#19d3f3", 63 | "#e763fa", 64 | "#fecb52", 65 | "#ffa15a", 66 | "#ff6692", 67 | "#b6e880", 68 | ], 69 | autosize: true, 70 | font: { color: "#f2f5fa" }, 71 | title: { x: 0.05 }, 72 | paper_bgcolor: theme.resolveVarToColor("--visualizer-background"), // "rgb(17,17,17)", 73 | plot_bgcolor: theme.resolveVarToColor("--visualizer-background"), // "rgb(17,17,17)", 74 | hovermode: "closest", 75 | polar: { 76 | bgcolor: theme.resolveVarToColor("--visualizer-background"), 77 | angularaxis: { 78 | gridcolor: "#506784", 79 | linecolor: "#506784", 80 | ticks: "", 81 | }, 82 | radialaxis: { 83 | gridcolor: "#506784", 84 | linecolor: "#506784", 85 | ticks: "", 86 | }, 87 | }, 88 | ternary: { 89 | bgcolor: theme.resolveVarToColor("--visualizer-background"), 90 | aaxis: { gridcolor: "#506784", linecolor: "#506784", ticks: "" }, 91 | baxis: { gridcolor: "#506784", linecolor: "#506784", ticks: "" }, 92 | caxis: { gridcolor: "#506784", linecolor: "#506784", ticks: "" }, 93 | }, 94 | xaxis: { 95 | gridcolor: "#283442", 96 | linecolor: "#506784", 97 | ticks: "", 98 | zerolinecolor: "#283442", 99 | zerolinewidth: 2, 100 | automargin: true, 101 | }, 102 | yaxis: { 103 | gridcolor: "#283442", 104 | linecolor: "#506784", 105 | ticks: "", 106 | zerolinecolor: "#283442", 107 | zerolinewidth: 2, 108 | automargin: true, 109 | }, 110 | scene: { 111 | xaxis: { 112 | gridcolor: "#506784", 113 | linecolor: "#506784", 114 | showbackground: false, 115 | ticks: "", 116 | zerolinecolor: "#C8D4E3", 117 | gridwidth: 2, 118 | }, 119 | yaxis: { 120 | gridcolor: "#506784", 121 | linecolor: "#506784", 122 | showbackground: false, 123 | ticks: "", 124 | zerolinecolor: "#C8D4E3", 125 | gridwidth: 2, 126 | }, 127 | zaxis: { 128 | gridcolor: "#506784", 129 | linecolor: "#506784", 130 | showbackground: false, 131 | ticks: "", 132 | zerolinecolor: "#C8D4E3", 133 | gridwidth: 2, 134 | }, 135 | }, 136 | geo: { 137 | bgcolor: theme.resolveVarToColor("--visualizer-plotly-background"), 138 | landcolor: theme.resolveVarToColor( 139 | "--visualizer-plotly-background" 140 | ), 141 | subunitcolor: "#506784", 142 | showland: true, 143 | showlakes: true, 144 | lakecolor: theme.resolveVarToColor( 145 | "--visualizer-plotly-background" 146 | ), 147 | }, 148 | template: { 149 | data: { 150 | table: [{ 151 | cells: { fill: { color: theme.resolveVarToColor("--visualizer-background") } }, 152 | header: { fill: { color: "rgb(17, 17, 17)" } }, 153 | }] 154 | } 155 | } 156 | }; 157 | } 158 | 159 | // extracted from the plotly website 160 | const darkLayout: Partial = { 161 | colorway: [ 162 | "#636efa", 163 | "#EF553B", 164 | "#00cc96", 165 | "#ab63fa", 166 | "#19d3f3", 167 | "#e763fa", 168 | "#fecb52", 169 | "#ffa15a", 170 | "#ff6692", 171 | "#b6e880", 172 | ], 173 | autosize: true, 174 | font: { color: "#f2f5fa" }, 175 | title: { x: 0.05 }, 176 | paper_bgcolor: "rgb(17,17,17)", 177 | plot_bgcolor: "rgb(17,17,17)", 178 | hovermode: "closest", 179 | polar: { 180 | bgcolor: "rgb(17,17,17)", 181 | angularaxis: { 182 | gridcolor: "#506784", 183 | linecolor: "#506784", 184 | ticks: "", 185 | }, 186 | radialaxis: { 187 | gridcolor: "#506784", 188 | linecolor: "#506784", 189 | ticks: "", 190 | }, 191 | }, 192 | ternary: { 193 | bgcolor: "rgb(17,17,17)", 194 | aaxis: { gridcolor: "#506784", linecolor: "#506784", ticks: "" }, 195 | baxis: { gridcolor: "#506784", linecolor: "#506784", ticks: "" }, 196 | caxis: { gridcolor: "#506784", linecolor: "#506784", ticks: "" }, 197 | }, 198 | xaxis: { 199 | gridcolor: "#283442", 200 | linecolor: "#506784", 201 | ticks: "", 202 | zerolinecolor: "#283442", 203 | zerolinewidth: 2, 204 | automargin: true, 205 | }, 206 | yaxis: { 207 | gridcolor: "#283442", 208 | linecolor: "#506784", 209 | ticks: "", 210 | zerolinecolor: "#283442", 211 | zerolinewidth: 2, 212 | automargin: true, 213 | }, 214 | scene: { 215 | xaxis: { 216 | backgroundcolor: "rgb(17,17,17)", 217 | gridcolor: "#506784", 218 | linecolor: "#506784", 219 | showbackground: true, 220 | ticks: "", 221 | zerolinecolor: "#C8D4E3", 222 | gridwidth: 2, 223 | }, 224 | yaxis: { 225 | backgroundcolor: "rgb(17,17,17)", 226 | gridcolor: "#506784", 227 | linecolor: "#506784", 228 | showbackground: true, 229 | ticks: "", 230 | zerolinecolor: "#C8D4E3", 231 | gridwidth: 2, 232 | }, 233 | zaxis: { 234 | backgroundcolor: "rgb(17,17,17)", 235 | gridcolor: "#506784", 236 | linecolor: "#506784", 237 | showbackground: true, 238 | ticks: "", 239 | zerolinecolor: "#C8D4E3", 240 | gridwidth: 2, 241 | }, 242 | }, 243 | geo: { 244 | bgcolor: "rgb(17,17,17)", 245 | landcolor: "rgb(17,17,17)", 246 | subunitcolor: "#506784", 247 | showland: true, 248 | showlakes: true, 249 | lakecolor: "rgb(17,17,17)", 250 | }, 251 | }; 252 | -------------------------------------------------------------------------------- /visualization-bundle/src/visualizers/plotly-visualizer/index.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | sArrayOf, 3 | sLiteral, 4 | sNull, 5 | sNumber, 6 | sOpenObject, 7 | sOptionalProp, 8 | sProp, 9 | sString, 10 | sUnion, 11 | } from "@hediet/semantic-json"; 12 | import { Deferred } from "@hediet/std/synchronization"; 13 | import { 14 | createLazyReactVisualization, 15 | createVisualizer, 16 | globalVisualizationFactory, 17 | } from "@hediet/visualization-core"; 18 | import * as React from "react"; 19 | import { visualizationNs } from "../../consts"; 20 | import { makeLazyLoadable } from "../../utils/LazyLoadable"; 21 | 22 | const PlotlyViewerLazyLoadable = makeLazyLoadable( 23 | async () => (await import("./PlotlyViewer")).PlotlyViewer 24 | ); 25 | 26 | const sDatum = sUnion([sString(), sNumber(), sNull()]); 27 | const sDatumArr = sUnion([sArrayOf(sDatum), sArrayOf(sArrayOf(sDatum))]); 28 | //const x: Plotly.Data; 29 | export const plotlyVisualizer = createVisualizer({ 30 | id: "plotly", 31 | name: "plotly", 32 | serializer: sOpenObject({ 33 | kind: sOpenObject({ 34 | plotly: sLiteral(true), 35 | }), 36 | data: sProp( 37 | sArrayOf( 38 | sOpenObject({ 39 | text: sOptionalProp( 40 | sUnion([sString(), sArrayOf(sString())]) 41 | ), 42 | xaxis: sOptionalProp(sString()), 43 | yaxis: sOptionalProp(sString()), 44 | x: sOptionalProp(sDatumArr), 45 | y: sOptionalProp(sDatumArr), 46 | z: sOptionalProp(sDatumArr), 47 | cells: sOptionalProp( 48 | sOpenObject({ 49 | values: sArrayOf(sDatumArr), 50 | }) 51 | ), 52 | header: sOptionalProp( 53 | sOpenObject({ 54 | values: sDatumArr, 55 | }) 56 | ), 57 | domain: sOptionalProp( 58 | sOpenObject({ 59 | x: sArrayOf(sNumber()), 60 | y: sArrayOf(sNumber()), 61 | }) 62 | ), 63 | type: sOptionalProp( 64 | sUnion([ 65 | sLiteral("bar"), 66 | sLiteral("box"), 67 | sLiteral("candlestick"), 68 | sLiteral("choropleth"), 69 | sLiteral("contour"), 70 | sLiteral("heatmap"), 71 | sLiteral("histogram"), 72 | sLiteral("indicator"), 73 | sLiteral("mesh3d"), 74 | sLiteral("ohlc"), 75 | sLiteral("parcoords"), 76 | sLiteral("pie"), 77 | sLiteral("pointcloud"), 78 | sLiteral("scatter"), 79 | sLiteral("scatter3d"), 80 | sLiteral("scattergeo"), 81 | sLiteral("scattergl"), 82 | sLiteral("scatterpolar"), 83 | sLiteral("scatterternary"), 84 | sLiteral("sunburst"), 85 | sLiteral("surface"), 86 | sLiteral("treemap"), 87 | sLiteral("waterfall"), 88 | sLiteral("funnel"), 89 | sLiteral("funnelarea"), 90 | sLiteral("scattermapbox"), 91 | sLiteral("table"), 92 | ]) 93 | ), 94 | mode: sOptionalProp( 95 | sUnion([ 96 | sLiteral("lines"), 97 | sLiteral("markers"), 98 | sLiteral("text"), 99 | sLiteral("lines+markers"), 100 | sLiteral("text+markers"), 101 | sLiteral("text+lines"), 102 | sLiteral("text+lines+markers"), 103 | sLiteral("none"), 104 | sLiteral("gauge"), 105 | sLiteral("number"), 106 | sLiteral("delta"), 107 | sLiteral("number+delta"), 108 | sLiteral("gauge+number"), 109 | sLiteral("gauge+number+delta"), 110 | sLiteral("gauge+delta"), 111 | ]) 112 | ), 113 | }) 114 | ), 115 | { 116 | description: 117 | "Expecting Plotly.Data[] (https://github.com/DefinitelyTyped/DefinitelyTyped/blob/795ce172038dbafcb9cba030d637d733a7eea19c/types/plotly.js/index.d.ts#L1036)", 118 | } 119 | ), 120 | layout: sOptionalProp( 121 | sOpenObject({ 122 | title: sOptionalProp(sString()), 123 | }), 124 | { 125 | description: 126 | "Expecting Partial (https://github.com/DefinitelyTyped/DefinitelyTyped/blob/795ce172038dbafcb9cba030d637d733a7eea19c/types/plotly.js/index.d.ts#L329)", 127 | } 128 | ), 129 | }).defineAs(visualizationNs("PlotlyVisualizationData")), 130 | getVisualization: (data, self) => 131 | createLazyReactVisualization( 132 | self, 133 | { 134 | priority: 1000, 135 | preload: PlotlyViewerLazyLoadable.preload, 136 | }, 137 | ({ theme }) => { 138 | const b = new Deferred(); 139 | return { 140 | node: ( 141 | b.resolve} 146 | /> 147 | ), 148 | ready: b.promise, 149 | }; 150 | } 151 | ), 152 | }); 153 | 154 | globalVisualizationFactory.addVisualizer(plotlyVisualizer); 155 | -------------------------------------------------------------------------------- /visualization-bundle/src/visualizers/plotly-visualizer/style.scss: -------------------------------------------------------------------------------- 1 | .themeable { 2 | --visualizer-plotly-background: var(--visualizer-background); 3 | } 4 | -------------------------------------------------------------------------------- /visualization-bundle/src/visualizers/simple-text-visualizer/index.tsx: -------------------------------------------------------------------------------- 1 | import { sOpenObject, sLiteral, sString } from "@hediet/semantic-json"; 2 | import * as React from "react"; 3 | import { 4 | createVisualizer, 5 | globalVisualizationFactory, 6 | createReactVisualization, 7 | } from "@hediet/visualization-core"; 8 | import { visualizationNs } from "../../consts"; 9 | 10 | export const simpleTextVisualizer = createVisualizer({ 11 | // The id must be unique 12 | id: "simple-text", 13 | // The name can be some display text 14 | name: "Simple Text", 15 | // Here you can define the schema for the data that this visualizer can handle. 16 | // You must have a kind property of type object whose properties must be `true`. 17 | serializer: sOpenObject({ 18 | kind: sOpenObject({ 19 | text: sLiteral(true), 20 | }), 21 | text: sString(), 22 | }).defineAs( 23 | // This name is important for schema and code generation of the interface types. 24 | // Make sure it is unique. It should end with "VisualizationData". 25 | visualizationNs("SimpleTextVisualizationData") 26 | ), 27 | getVisualization: ( 28 | /* The type of data is specified by the serializer above. */ data, 29 | self 30 | ) => 31 | // If you want to use react, this is the way to go. 32 | // Return your own implementation if you want to render directly to the DOM. 33 | createReactVisualization( 34 | self, 35 | { 36 | // The priority is used for automatically selecting the best visualization 37 | // if multiple visualizations can handle the data. 38 | priority: 100, 39 | }, 40 | () => ( 41 |
53 | 					{data.text}
54 | 				
55 | ) 56 | ), 57 | }); 58 | 59 | // This registers the visualizer for automatic discovery. 60 | // Make sure to import this file in "../index.ts" so that it gets loaded when 61 | // someone imports "@hediet/visualization-bundle"! 62 | globalVisualizationFactory.addVisualizer(simpleTextVisualizer); 63 | -------------------------------------------------------------------------------- /visualization-bundle/src/visualizers/simple-text-visualizer/style.scss: -------------------------------------------------------------------------------- 1 | // Provide default colors for both the dark and light theme here! 2 | 3 | .themeable { 4 | --visualizer-simple-text-background: var(--visualizer-background); 5 | } 6 | .theme-dark { 7 | --visualizer-simple-text-color: white; 8 | } 9 | .theme-light { 10 | --visualizer-simple-text-color: black; 11 | } 12 | -------------------------------------------------------------------------------- /visualization-bundle/src/visualizers/source-visualizer/index.tsx: -------------------------------------------------------------------------------- 1 | import { JsonSchemaGenerator, sOpenObject } from "@hediet/semantic-json"; 2 | import * as React from "react"; 3 | import { 4 | createReactVisualization, 5 | createVisualizer, 6 | globalVisualizationFactory, 7 | } from "@hediet/visualization-core"; 8 | import { visualizationNs } from "../../consts"; 9 | import { MonacoEditorLazyLoadable } from "../monaco-text-visualizer"; 10 | 11 | export const sourceVisualizer = createVisualizer({ 12 | id: "source", 13 | name: "JSON Source", 14 | serializer: sOpenObject({ 15 | kind: sOpenObject({}), 16 | }).defineAs(visualizationNs("SourceVisualizationData")), 17 | getVisualization: (data, self) => 18 | createReactVisualization(self, { priority: -100 }, ({ theme }) => { 19 | const s = new JsonSchemaGenerator(); 20 | 21 | return ( 22 | 34 | ); 35 | }), 36 | }); 37 | 38 | globalVisualizationFactory.addHiddenVisualizer(sourceVisualizer); 39 | -------------------------------------------------------------------------------- /visualization-bundle/src/visualizers/source-visualizer/style.scss: -------------------------------------------------------------------------------- 1 | .theme-dark { 2 | --visualizer-simple-text-color: white; 3 | } 4 | .theme-light { 5 | --visualizer-simple-text-color: black; 6 | } 7 | -------------------------------------------------------------------------------- /visualization-bundle/src/visualizers/svg-visualizer/SvgViewer.tsx: -------------------------------------------------------------------------------- 1 | import Measure, { ContentRect } from "react-measure"; 2 | import { observer } from "mobx-react"; 3 | import * as React from "react"; 4 | import { observable, action } from "mobx"; 5 | import { Tool, ReactSVGPanZoom, Value } from "react-svg-pan-zoom"; 6 | 7 | function widthOrDefault(r: ContentRect): number { 8 | if (r.bounds && r.bounds.width) { 9 | return r.bounds.width; 10 | } 11 | return 1; 12 | } 13 | 14 | function heightOrDefault(r: ContentRect): number { 15 | if (r.bounds && r.bounds.height) { 16 | return r.bounds.height; 17 | } 18 | return 1; 19 | } 20 | 21 | @observer 22 | export class SvgViewer extends React.Component<{ 23 | svgContent: string; 24 | svgRef?: (element: SVGSVGElement | null) => void; 25 | }> { 26 | @observable tool: Tool = "pan"; 27 | 28 | @action.bound 29 | private setTool(tool: Tool): void { 30 | this.tool = tool; 31 | } 32 | 33 | private readonly svgPanZoomRef = React.createRef(); 34 | 35 | componentDidMount() { 36 | const element = this.svgPanZoomRef.current; 37 | if (this.props.svgRef) { 38 | if (!element) { 39 | this.props.svgRef(null); 40 | return; 41 | } 42 | 43 | const svg = (element as any).ViewerDOM as SVGSVGElement; 44 | this.props.svgRef(svg); 45 | } 46 | } 47 | 48 | @observable value: Value | {} = {}; 49 | 50 | render() { 51 | let { svgContent } = this.props; 52 | let width: number = 0; 53 | let height: number = 0; 54 | svgContent = svgContent.replace( 55 | /viewBox="[0-9\.]+ [0-9\.]+ ([0-9\.]+) ([0-9\.]+)"/, 56 | (r, w, h) => { 57 | width = parseFloat(w); 58 | height = parseFloat(h); 59 | return ""; 60 | } 61 | ); 62 | const tool = this.tool; 63 | const val = this.value; 64 | 65 | return ( 66 | { 69 | if (this.svgPanZoomRef.current) { 70 | (this.svgPanZoomRef.current.fitToViewer as any)( 71 | "center", 72 | "center" 73 | ); 74 | } 75 | }} 76 | > 77 | {({ 78 | measureRef, 79 | contentRect, 80 | }: { 81 | measureRef: any; 82 | contentRect: any; 83 | }) => ( 84 |
89 | (this.value = v)} 95 | onChangeTool={this.setTool} 96 | ref={this.svgPanZoomRef} 97 | toolbarProps={{ 98 | SVGAlignX: "center", 99 | SVGAlignY: "center", 100 | }} 101 | miniatureProps={{ 102 | position: "none", 103 | height: 0, 104 | width: 0, 105 | background: "black", 106 | }} 107 | > 108 | 109 | 114 | 115 | 116 |
117 | )} 118 |
119 | ); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /visualization-bundle/src/visualizers/svg-visualizer/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { sOpenObject, sLiteral, sString, sProp } from "@hediet/semantic-json"; 3 | import { SvgViewer } from "./SvgViewer"; 4 | import { 5 | createVisualizer, 6 | globalVisualizationFactory, 7 | createReactVisualization, 8 | } from "@hediet/visualization-core"; 9 | import { visualizationNs } from "../../consts"; 10 | 11 | export const svgVisualizer = createVisualizer({ 12 | id: "svg", 13 | name: "Svg", 14 | serializer: sOpenObject({ 15 | kind: sOpenObject({ 16 | svg: sLiteral(true), 17 | }), 18 | text: sProp(sString(), { description: "The svg content" }), 19 | }).defineAs(visualizationNs("SvgVisualizationData")), 20 | getVisualization: (data, self) => 21 | createReactVisualization(self, { priority: 1500 }, () => ( 22 | 23 | )), 24 | }); 25 | 26 | globalVisualizationFactory.addVisualizer(svgVisualizer); 27 | -------------------------------------------------------------------------------- /visualization-bundle/src/visualizers/svg-visualizer/style.scss: -------------------------------------------------------------------------------- 1 | .svgViewer { 2 | svg { 3 | overflow: auto; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /visualization-bundle/src/visualizers/tree-visualizer/Point.ts: -------------------------------------------------------------------------------- 1 | function sqr(a: number) { 2 | return a * a; 3 | } 4 | 5 | export type PointLike = 6 | | Point 7 | | { x?: number; y: number } 8 | | { x: number; y?: number }; 9 | 10 | export function point(data: PointLike) { 11 | if (data instanceof Point) { 12 | return data; 13 | } 14 | return new Point(data.x || 0, data.y || 0); 15 | } 16 | 17 | export class Point { 18 | public static readonly Zero = new Point(0, 0); 19 | 20 | constructor(public readonly x: number, public readonly y: number) {} 21 | 22 | public mapXY(fn: (arg: number) => number): Point { 23 | return new Point(fn(this.x), fn(this.y)); 24 | } 25 | 26 | public distance(other: PointLike = Point.Zero): number { 27 | const d = this.sub(other); 28 | return Math.sqrt(sqr(d.x) + sqr(d.y)); 29 | } 30 | 31 | public sub(other: PointLike): Point { 32 | const o = point(other); 33 | return new Point(this.x - o.x, this.y - o.y); 34 | } 35 | 36 | public add(other: PointLike): Point { 37 | const o = point(other); 38 | return new Point(this.x + o.x, this.y + o.y); 39 | } 40 | 41 | public mul(scalar: number): Point { 42 | return new Point(this.x * scalar, this.y * scalar); 43 | } 44 | 45 | public div(scalar: number): Point { 46 | return new Point(this.x / scalar, this.y / scalar); 47 | } 48 | 49 | public equals(other: PointLike) { 50 | const o = point(other); 51 | return this.x === o.x && this.y === o.y; 52 | } 53 | 54 | public angle(): number { 55 | const angle = Math.atan2(this.x, this.y); 56 | return angle; 57 | } 58 | 59 | public getPointCloserTo(dest: PointLike, dist: number): Point { 60 | if (this.equals(dest)) return this; 61 | 62 | const angle = point(dest) 63 | .sub(this) 64 | .angle(); 65 | 66 | const result = new Point( 67 | this.x + Math.sin(angle) * dist, 68 | this.y + Math.cos(angle) * dist 69 | ); 70 | return result; 71 | } 72 | 73 | public toJson(): { x: number; y: number } { 74 | return { x: this.x, y: this.y }; 75 | } 76 | } 77 | 78 | function turn(p1: Point, p2: Point, p3: Point): number { 79 | const A = (p3.y - p1.y) * (p2.x - p1.x); 80 | const B = (p2.y - p1.y) * (p3.x - p1.x); 81 | return A > B + Number.MIN_VALUE ? 1 : A + Number.MIN_VALUE < B ? -1 : 0; 82 | } 83 | 84 | export function isIntersect( 85 | aStart: Point, 86 | aEnd: Point, 87 | bStart: Point, 88 | bEnd: Point 89 | ): boolean { 90 | return ( 91 | turn(aStart, bStart, bEnd) != turn(aEnd, bStart, bEnd) && 92 | turn(aStart, aEnd, bStart) != turn(aStart, aEnd, bEnd) 93 | ); 94 | } 95 | 96 | /** 97 | * Liang-Barsky function by Daniel White 98 | * 99 | * @link http://www.skytopia.com/project/articles/compsci/clipping.html 100 | * 101 | * @param {number} x0 102 | * @param {number} y0 103 | * @param {number} x1 104 | * @param {number} y1 105 | * @param {array} bbox 106 | * @return {array>|null} 107 | */ 108 | function liangBarsky( 109 | x0: number, 110 | y0: number, 111 | x1: number, 112 | y1: number, 113 | bbox: [number, number, number, number] 114 | ): [[number, number], [number, number]] | null { 115 | let [xmin, xmax, ymin, ymax] = bbox; 116 | let t0 = 0, 117 | t1 = 1; 118 | let dx = x1 - x0, 119 | dy = y1 - y0; 120 | let p = 0, 121 | q = 0, 122 | r = 0; 123 | 124 | for (let edge: 0 | 1 | 2 | 3 = 0; edge < 4; edge++) { 125 | // Traverse through left, right, bottom, top edges. 126 | if (edge === 0) { 127 | p = -dx; 128 | q = -(xmin - x0); 129 | } else if (edge === 1) { 130 | p = dx; 131 | q = xmax - x0; 132 | } else if (edge === 2) { 133 | p = -dy; 134 | q = -(ymin - y0); 135 | } else if (edge === 3) { 136 | p = dy; 137 | q = ymax - y0; 138 | } 139 | 140 | r = q / p; 141 | 142 | if (p === 0 && q < 0) return null; // Don't draw line at all. (parallel line outside) 143 | 144 | if (p < 0) { 145 | if (r > t1) return null; 146 | // Don't draw line at all. 147 | else if (r > t0) t0 = r; // Line is clipped! 148 | } else if (p > 0) { 149 | if (r < t0) return null; 150 | // Don't draw line at all. 151 | else if (r < t1) t1 = r; // Line is clipped! 152 | } 153 | } 154 | 155 | return [[x0 + t0 * dx, y0 + t0 * dy], [x0 + t1 * dx, y0 + t1 * dy]]; 156 | } 157 | 158 | export function intersectRectWithLine( 159 | start: Point, 160 | end: Point, 161 | rect: Rectangle 162 | ): { first: Point; second: Point } | undefined { 163 | const r = liangBarsky(start.x, start.y, end.x, end.y, [ 164 | rect.topLeft.x, 165 | rect.bottomRight.x, 166 | rect.topLeft.y, 167 | rect.bottomRight.y, 168 | ]); 169 | if (!r) { 170 | return undefined; 171 | } 172 | 173 | return { 174 | first: new Point(r[0][0], r[0][1]), 175 | second: new Point(r[1][0], r[1][1]), 176 | }; 177 | } 178 | 179 | // first zoom then translate 180 | export function scale( 181 | clientOffset: Point, 182 | clientSize: Point, 183 | viewSize: Point 184 | ): { clientZoom: number; clientOffset: Point } { 185 | const clientRatio = clientSize.x / clientSize.y; 186 | const viewRatio = viewSize.x / viewSize.y; 187 | 188 | let zoom = 1; 189 | 190 | if (clientRatio < viewRatio) zoom = viewSize.y / clientSize.y; 191 | else zoom = viewSize.x / clientSize.x; 192 | 193 | const clientMid = clientOffset.mul(zoom).add(clientSize.mul(zoom / 2)); 194 | const viewMid = viewSize.div(2); 195 | 196 | const clientOffset2 = viewMid.sub(clientMid); 197 | 198 | return { clientOffset: clientOffset2, clientZoom: zoom }; 199 | } 200 | 201 | export class Rectangle { 202 | public static ofSize(position: PointLike, size: PointLike): Rectangle { 203 | const pos = point(position); 204 | return new Rectangle(pos, pos.add(size)); 205 | } 206 | 207 | public static spanning(first: Point, ...rest: Point[]): Rectangle { 208 | let min = first.toJson(); 209 | let max = first.toJson(); 210 | for (const p of rest) { 211 | min.x = Math.min(min.x, p.x); 212 | min.y = Math.min(min.y, p.y); 213 | max.x = Math.max(max.x, p.x); 214 | max.y = Math.max(max.y, p.y); 215 | } 216 | 217 | return new Rectangle(point(min), point(max)); 218 | } 219 | 220 | constructor( 221 | public readonly topLeft: Point, 222 | public readonly bottomRight: Point 223 | ) {} 224 | 225 | get center(): Point { 226 | return this.bottomRight.add(this.topLeft).div(2); 227 | } 228 | 229 | get size(): Point { 230 | return this.bottomRight.sub(this.topLeft); 231 | } 232 | 233 | get topRight(): Point { 234 | return new Point(this.bottomRight.x, this.topLeft.y); 235 | } 236 | 237 | get bottomLeft(): Point { 238 | return new Point(this.topLeft.x, this.bottomRight.y); 239 | } 240 | 241 | public intersects(selectionRect: Rectangle): boolean { 242 | return !( 243 | selectionRect.topLeft.x > this.bottomRight.x || 244 | selectionRect.bottomRight.x < this.topLeft.x || 245 | selectionRect.topLeft.y > this.bottomRight.y || 246 | selectionRect.bottomRight.y < this.topLeft.y 247 | ); 248 | } 249 | 250 | equals(other: Rectangle) { 251 | return ( 252 | this.topLeft.equals(other.topLeft) && 253 | this.bottomRight.equals(other.bottomRight) 254 | ); 255 | } 256 | } 257 | -------------------------------------------------------------------------------- /visualization-bundle/src/visualizers/tree-visualizer/SvgElements.tsx: -------------------------------------------------------------------------------- 1 | import { Point, Rectangle } from "./Point"; 2 | import * as React from "react"; 3 | import { Properties } from "csstype"; 4 | 5 | type Omit = Pick>; 6 | 7 | export function omit( 8 | obj: T, 9 | keys: K[] 10 | ): Omit { 11 | const newObj: Omit = {} as any; 12 | for (const [key, val] of Object.entries(obj)) { 13 | if (!keys.includes(key as K)) { 14 | (newObj as any)[key] = val; 15 | } 16 | } 17 | return newObj; 18 | } 19 | 20 | export interface SvgAttributes extends React.DOMAttributes { 21 | stroke?: string; 22 | className?: string; 23 | style?: Properties; 24 | pointerEvents?: "none" | "auto" | "initial"; 25 | } 26 | 27 | export function SvgText( 28 | props: { 29 | position: Point; 30 | children: any; 31 | childRef?: React.Ref; 32 | textAnchor?: "middle" | "end" | "start"; 33 | dominantBaseline?: "central" | "middle"; 34 | } & SvgAttributes 35 | ) { 36 | return ( 37 | 43 | ); 44 | } 45 | 46 | export function SvgCircle( 47 | props: { 48 | center: Point; 49 | radius: number; 50 | childRef?: React.Ref; 51 | } & SvgAttributes 52 | ) { 53 | return ( 54 | 61 | ); 62 | } 63 | 64 | export function SvgLine( 65 | props: { start: Point; end: Point } & SvgAttributes 66 | ) { 67 | return ( 68 | 75 | ); 76 | } 77 | 78 | export function SvgRect( 79 | props: { rectangle: Rectangle; fill?: string } & SvgAttributes< 80 | SVGRectElement 81 | > 82 | ) { 83 | const r = props.rectangle; 84 | return ( 85 | 92 | ); 93 | } 94 | -------------------------------------------------------------------------------- /visualization-bundle/src/visualizers/tree-visualizer/Views.tsx: -------------------------------------------------------------------------------- 1 | import { observer, disposeOnUnmount } from "mobx-react"; 2 | import { observable, autorun, computed, action, runInAction } from "mobx"; 3 | import * as React from "react"; 4 | import classNames from "classnames"; 5 | import { Rectangle, Point } from "./Point"; 6 | import { SvgLine } from "./SvgElements"; 7 | import { Item } from "."; 8 | 9 | // import "./style.scss"; 10 | 11 | export class TreeViewModel { 12 | private started = true; 13 | start() { 14 | this.started = true; 15 | this.updateOffsets(); 16 | } 17 | stop() { 18 | this.started = false; 19 | } 20 | 21 | private version = 0; 22 | private lastUpdateVersion = -1; 23 | 24 | @observable public root: TreeNodeViewModel | undefined = undefined; 25 | 26 | public invalidateOffsets() { 27 | this.version++; 28 | } 29 | 30 | @action 31 | public updateOffsets() { 32 | if (!this.started) { 33 | return; 34 | } 35 | if (!this.root) { 36 | return; 37 | } 38 | if (this.lastUpdateVersion === this.version) { 39 | return; 40 | } 41 | 42 | this.lastUpdateVersion = this.version; 43 | console.log("update"); 44 | this.root.updateOffsets(); 45 | } 46 | 47 | public get marked(): TreeNodeViewModel[] { 48 | const result = new Array>(); 49 | function recurse(node: TreeNodeViewModel) { 50 | if (node.isMarked) { 51 | result.push(node); 52 | } 53 | for (const c of node.children) { 54 | recurse(c); 55 | } 56 | } 57 | if (this.root) { 58 | recurse(this.root); 59 | } 60 | return result; 61 | } 62 | 63 | @observable selected: TreeNodeViewModel | undefined = undefined; 64 | 65 | @action public select(current: TreeNodeViewModel | undefined) { 66 | if (this.selected === current) { 67 | return; 68 | } 69 | if (this.selected) { 70 | this.selected.isSelected = false; 71 | } 72 | if (current) { 73 | current.isSelected = true; 74 | } 75 | this.selected = current; 76 | } 77 | 78 | @action public toggleSelect(current: TreeNodeViewModel) { 79 | if (this.selected === current) { 80 | this.select(undefined); 81 | } else { 82 | this.select(current); 83 | } 84 | } 85 | 86 | private selectOnHoverEnabled = false; 87 | private selectedBeforeHover: 88 | | TreeNodeViewModel 89 | | undefined = undefined; 90 | 91 | @action public handleHoverEvent( 92 | current: TreeNodeViewModel | undefined 93 | ) { 94 | if (this.selectOnHoverEnabled) { 95 | if (current) { 96 | this.select(current); 97 | } else { 98 | this.select(this.selectedBeforeHover); 99 | } 100 | } 101 | } 102 | 103 | setSelectOnHover(enable: boolean) { 104 | if (this.selectOnHoverEnabled === enable) { 105 | return; 106 | } 107 | 108 | if (enable) { 109 | this.selectedBeforeHover = this.selected; 110 | } else if (!this.selected) { 111 | this.select(this.selectedBeforeHover); 112 | } 113 | 114 | this.selectOnHoverEnabled = enable; 115 | } 116 | } 117 | 118 | export class TreeNodeViewModel { 119 | public parent: TreeNodeViewModel | null = null; 120 | 121 | constructor( 122 | public readonly treeViewModel: TreeViewModel, 123 | public readonly children: TreeNodeViewModel[], 124 | public readonly items: Item[], 125 | public readonly segment: string, 126 | public readonly data: TData 127 | ) {} 128 | 129 | @observable expanderOffsetRect: Rectangle | undefined = undefined; 130 | private expanderDiv: HTMLDivElement | null = null; 131 | 132 | @action.bound 133 | setExpanderDiv(div: HTMLDivElement | null) { 134 | this.expanderDiv = div; 135 | this.treeViewModel.invalidateOffsets(); 136 | } 137 | 138 | @observable public expanded: boolean = false; 139 | @observable public isMarked: boolean = false; 140 | @observable public isSelected: boolean = false; 141 | 142 | @action 143 | public toggleExpanded() { 144 | this.expanded = !this.expanded; 145 | this.treeViewModel.invalidateOffsets(); 146 | } 147 | 148 | @computed public get isChildOrThisMarked(): boolean { 149 | if (this.isMarked) return true; 150 | return this.children.some(c => c.isChildOrThisMarked); 151 | } 152 | 153 | @computed public get isChildOrThisSelected(): boolean { 154 | if (this.isSelected) return true; 155 | return this.children.some(c => c.isChildOrThisSelected); 156 | } 157 | 158 | @computed public get isParentOrThisSelected(): boolean { 159 | if (this.isSelected) return true; 160 | if (this.parent) return this.parent.isParentOrThisSelected; 161 | return false; 162 | } 163 | 164 | get path(): string[] { 165 | const result = new Array(); 166 | let c: TreeNodeViewModel = this; 167 | while (c.parent) { 168 | result.unshift(c.segment); 169 | c = c.parent; 170 | } 171 | if (result.length === 0) { 172 | result.push("root"); 173 | } 174 | return result; 175 | } 176 | 177 | updateOffsets() { 178 | for (const c of this.children) { 179 | c.updateOffsets(); 180 | } 181 | 182 | if (!this.expanderDiv) { 183 | return; 184 | } 185 | const newRect = Rectangle.ofSize( 186 | new Point(this.expanderDiv.offsetLeft, this.expanderDiv.offsetTop), 187 | new Point( 188 | this.expanderDiv.offsetWidth, 189 | this.expanderDiv.offsetHeight 190 | ) 191 | ); 192 | if ( 193 | !this.expanderOffsetRect || 194 | !newRect.equals(this.expanderOffsetRect) 195 | ) { 196 | this.expanderOffsetRect = newRect; 197 | } 198 | } 199 | } 200 | 201 | const isValidFunctionName = (function() { 202 | var validName = /^[$A-Z_][0-9A-Z_$]*$/i; 203 | var reserved = { 204 | abstract: true, 205 | boolean: true, 206 | // ... 207 | with: true, 208 | } as any; 209 | return function(s: string) { 210 | // Ensure a valid name and not reserved. 211 | return validName.test(s) && !reserved[s]; 212 | }; 213 | })(); 214 | 215 | @observer 216 | export class TreeWithPathView extends React.Component<{ 217 | model: TreeViewModel; 218 | }> { 219 | render() { 220 | const model = this.props.model; 221 | return ( 222 |
223 |
224 | 225 | {model.selected ? ( 226 | 227 | {model.selected.path.reduce((acc, v) => { 228 | acc = acc.slice(); 229 | acc.push( 230 | 234 | {v} 235 | 236 | ); 237 | return acc; 238 | }, new Array())} 239 | 240 | ) : ( 241 | "(Nothing Selected)" 242 | )} 243 | 244 |
245 |
246 | 247 |
248 |
249 | ); 250 | } 251 | } 252 | 253 | @observer 254 | export class TreeView extends React.Component<{ model: TreeViewModel }> { 255 | componentWillUpdate(newProps: this["props"]) { 256 | newProps.model.stop(); 257 | } 258 | 259 | componentWillMount() { 260 | this.props.model.stop(); 261 | } 262 | 263 | componentDidUpdate() { 264 | this.props.model.start(); 265 | } 266 | 267 | componentDidMount() { 268 | this.props.model.start(); 269 | } 270 | 271 | render() { 272 | const model = this.props.model; 273 | return ( 274 |
{ 278 | if (e.keyCode === 18) { 279 | // alt 280 | e.preventDefault(); 281 | e.stopPropagation(); 282 | model.setSelectOnHover(true); 283 | } 284 | }} 285 | onKeyUp={e => { 286 | if (e.keyCode === 18) { 287 | // alt 288 | e.preventDefault(); 289 | e.stopPropagation(); 290 | model.setSelectOnHover(false); 291 | } 292 | }} 293 | > 294 | {model.root && ( 295 | <> 296 | 297 | 298 | 299 |
300 | 301 |
302 | 303 | )} 304 |
305 | ); 306 | } 307 | } 308 | 309 | @observer 310 | export class TreeNodeSvgView extends React.Component<{ 311 | model: TreeNodeViewModel; 312 | }> { 313 | render() { 314 | const model = this.props.model; 315 | if ( 316 | !model.expanderOffsetRect || 317 | model.children.length === 0 || 318 | model.expanded 319 | ) { 320 | return ; 321 | } 322 | 323 | function findLastExpanderOffset( 324 | predicate: (v: TreeNodeViewModel) => boolean 325 | ): Rectangle | undefined { 326 | const all = model.children 327 | .filter(predicate) 328 | .map(c => c.expanderOffsetRect); 329 | const last = all[all.length - 1]; 330 | return last; 331 | } 332 | 333 | const lastOffset = findLastExpanderOffset(() => true); 334 | const selectedOffset = findLastExpanderOffset( 335 | v => v.isChildOrThisSelected 336 | ); 337 | const markedOffset = findLastExpanderOffset(v => v.isChildOrThisMarked); 338 | 339 | if (!lastOffset) { 340 | return ; 341 | } 342 | 343 | const start = model.expanderOffsetRect.center.add({ y: 10 }); 344 | 345 | return ( 346 | 347 | {markedOffset && ( 348 | 353 | )} 354 | {selectedOffset && ( 355 | 360 | )} 361 | 366 | {model.children.map((c, idx) => { 367 | const end = c.expanderOffsetRect!.center.sub({ x: 5 }); 368 | 369 | return ( 370 | 371 | {c.isChildOrThisMarked && ( 372 | 380 | )} 381 | 390 | 391 | ); 392 | })} 393 | {model.children.map((c, idx) => ( 394 | 395 | ))} 396 | 397 | ); 398 | } 399 | } 400 | 401 | @observer 402 | export class TreeNodeView extends React.Component<{ 403 | model: TreeNodeViewModel; 404 | }> { 405 | @action.bound 406 | private clickHandler() { 407 | this.props.model.treeViewModel.toggleSelect(this.props.model); 408 | } 409 | 410 | private handleMouseEnterOrLeave(enter: boolean) { 411 | this.props.model.treeViewModel.handleHoverEvent( 412 | enter ? this.props.model : undefined 413 | ); 414 | } 415 | 416 | @observable 417 | private rootDiv: HTMLElement | null = null; 418 | @action.bound 419 | private setRootDiv(div: HTMLElement | null) { 420 | this.rootDiv = div; 421 | } 422 | 423 | @disposeOnUnmount 424 | private readonly _autorun = autorun(() => { 425 | if (this.props.model.isMarked && this.rootDiv) { 426 | this.rootDiv.scrollIntoView({ block: "nearest" }); 427 | } 428 | }); 429 | 430 | public componentDidMount() { 431 | this.props.model.treeViewModel.updateOffsets(); 432 | } 433 | 434 | public componentDidUpdate() { 435 | this.props.model.treeViewModel.updateOffsets(); 436 | } 437 | 438 | public render(): JSX.Element { 439 | const model = this.props.model; 440 | const collapsed = model.expanded; 441 | console.log("render"); 442 | return ( 443 |
458 |
{ 461 | this.handleMouseEnterOrLeave(true); 462 | e.stopPropagation(); 463 | }} 464 | onMouseOut={e => { 465 | this.handleMouseEnterOrLeave(false); 466 | e.stopPropagation(); 467 | }} 468 | > 469 |
model.toggleExpanded()} 472 | className={classNames( 473 | "part-collapser", 474 | model.children.length > 0 && 475 | "decorator-collapse-icon", 476 | !collapsed && "expanded" 477 | )} 478 | /> 479 | 480 | 481 | {renderItems(model.items)} 482 | 483 |
484 | 485 | {model.children.length > 0 && !model.expanded && ( 486 |
487 | {model.children.map((c, idx) => ( 488 | 489 | ))} 490 |
491 | )} 492 |
493 | ); 494 | } 495 | } 496 | 497 | function renderItems(items: Item[]): React.ReactNode { 498 | return items.map((i, idx) => { 499 | switch (i.emphasis) { 500 | case "style1": 501 | return ( 502 | 503 | {i.text} 504 | 505 | ); 506 | case "style2": 507 | return ( 508 | 509 | {i.text} 510 | 511 | ); 512 | case "style3": 513 | return ( 514 | 515 | {i.text} 516 | 517 | ); 518 | default: 519 | return ( 520 | 521 | {i.text} 522 | 523 | ); 524 | } 525 | }); 526 | } 527 | -------------------------------------------------------------------------------- /visualization-bundle/src/visualizers/tree-visualizer/index.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | TreeViewModel, 3 | TreeNodeViewModel, 4 | TreeView, 5 | TreeWithPathView, 6 | } from "./Views"; 7 | import * as React from "react"; 8 | import { 9 | sOpenObject, 10 | sLiteral, 11 | sLazy, 12 | sString, 13 | sBoolean, 14 | sArrayOf, 15 | Serializer, 16 | sOptionalProp, 17 | sUnion, 18 | sProp, 19 | } from "@hediet/semantic-json"; 20 | import { 21 | createVisualizer, 22 | globalVisualizationFactory, 23 | createReactVisualization, 24 | } from "@hediet/visualization-core"; 25 | import { visualizationNs } from "../../consts"; 26 | 27 | type TreeNode = { 28 | children: TreeNode[]; 29 | items: Item[]; 30 | segment?: string; // like "root", ".name" or "[0]" 31 | isMarked?: boolean; 32 | } & T; 33 | 34 | export interface Item { 35 | text: string; 36 | emphasis?: "style1" | "style2" | "style3" | string; 37 | } 38 | 39 | const sTreeNode: Serializer = sLazy(() => 40 | sOpenObject({ 41 | children: sProp(sArrayOf(sTreeNode), { 42 | description: "The children of this tree-node", 43 | }), 44 | items: sProp( 45 | sArrayOf( 46 | sOpenObject({ 47 | text: sProp(sString(), { description: "The text to show" }), 48 | emphasis: sOptionalProp( 49 | sUnion( 50 | [ 51 | sLiteral("style1"), 52 | sLiteral("style2"), 53 | sLiteral("style3"), 54 | sString(), 55 | ], 56 | { inclusive: true } 57 | ), 58 | { 59 | description: "The style of the text", 60 | } 61 | ), 62 | }).defineAs(visualizationNs("TreeNodeItem")) 63 | ), 64 | { description: "The parts that make up the text of this item" } 65 | ), 66 | segment: sOptionalProp(sString(), { 67 | description: 68 | "If a node is selected, the concatenation of all segment values from root to the selected node is shown to the user.", 69 | }), 70 | isMarked: sOptionalProp(sBoolean(), { 71 | description: 72 | "Marked nodes are highlighted and scrolled into view on every visualization update.", 73 | }), 74 | }).defineAs(visualizationNs("TreeNode")) 75 | ); 76 | 77 | const sTree = sOpenObject({ 78 | kind: sOpenObject({ 79 | tree: sLiteral(true), 80 | }), 81 | root: sTreeNode, 82 | }).defineAs(visualizationNs("TreeVisualizationData")); 83 | 84 | export const treeVisualizer = createVisualizer({ 85 | id: "tree", 86 | name: "Tree", 87 | serializer: sTree, 88 | getVisualization: (data, self) => 89 | createReactVisualization(self, { priority: 1200 }, () => { 90 | const m = createTreeViewModelFromTreeNodeData( 91 | data.root, 92 | () => undefined 93 | ); 94 | return ; 95 | }), 96 | }); 97 | 98 | globalVisualizationFactory.addVisualizer(treeVisualizer); 99 | 100 | export function createTreeViewModelFromTreeNodeData( 101 | root: TreeNode, 102 | dataSelector: (node: TreeNode) => TData 103 | ): TreeViewModel { 104 | const m = new TreeViewModel(); 105 | m.root = recurse(root, m); 106 | return m; 107 | 108 | function recurse( 109 | node: TreeNode, 110 | viewModel: TreeViewModel 111 | ): TreeNodeViewModel { 112 | const children: TreeNodeViewModel[] = node.children.map(c => 113 | recurse(c, viewModel) 114 | ); 115 | 116 | let segment; 117 | if (node.segment) { 118 | segment = node.segment; 119 | } else if (node.items.length > 0) { 120 | segment = node.items[0].text; 121 | } else { 122 | segment = ""; 123 | } 124 | 125 | const model = new TreeNodeViewModel( 126 | viewModel, 127 | children, 128 | node.items, 129 | segment, 130 | dataSelector(node) 131 | ); 132 | model.isMarked = !!node.isMarked; 133 | for (const c of children) { 134 | c.parent = model; 135 | } 136 | return model; 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /visualization-bundle/src/visualizers/tree-visualizer/style.scss: -------------------------------------------------------------------------------- 1 | .themeable { 2 | --visualizer-tree-background: var(--visualizer-background); 3 | } 4 | 5 | .theme-dark { 6 | --visualizer-tree-childOrThisMarked-background: orange; 7 | --visualizer-tree-childOrThisSelected-background: white; 8 | --visualizer-tree-pathColor: gray; 9 | 10 | --visualizer-tree-style1-foreground: #5594ea; 11 | --visualizer-tree-style2-foreground: #c88544; //#38b968; 12 | --visualizer-tree-style3-foreground: darkred; 13 | --visualizer-tree-style3-border: black; 14 | --visualizer-tree-unstyled-foreground: lightgray; 15 | --visualizer-tree-path-foreground: white; 16 | } 17 | 18 | .theme-light { 19 | --visualizer-tree-childOrThisMarked-background: orange; 20 | --visualizer-tree-childOrThisSelected-background: black; 21 | --visualizer-tree-pathColor: gray; 22 | 23 | --visualizer-tree-style1-foreground: blue; 24 | --visualizer-tree-style2-foreground: darkgreen; 25 | --visualizer-tree-style3-foreground: darkred; 26 | --visualizer-tree-style3-border: black; 27 | --visualizer-tree-unstyled-foreground: black; 28 | --visualizer-tree-path-foreground: black; 29 | } 30 | 31 | .component-TreeView { 32 | overflow: auto; 33 | scroll-behavior: smooth; 34 | height: 100%; 35 | position: relative; 36 | display: grid; 37 | 38 | & > .part-svg, 39 | & > .part-node { 40 | height: 100%; 41 | width: 100%; 42 | grid-column: 1; 43 | grid-row: 1; 44 | } 45 | } 46 | 47 | .component-TreeNodeSvgView { 48 | .path { 49 | stroke: var(--visualizer-tree-pathColor); 50 | stroke-dasharray: 1; 51 | 52 | &.childOrThisMarked { 53 | stroke: var(--visualizer-tree-childOrThisMarked-background); 54 | stroke-width: 5; 55 | stroke-dasharray: 0; 56 | stroke-linecap: round; 57 | } 58 | 59 | &.childOrThisSelected { 60 | stroke: var(--visualizer-tree-childOrThisSelected-background); 61 | stroke-width: 2; 62 | stroke-dasharray: 0; 63 | } 64 | } 65 | } 66 | 67 | .component-TreeNodeView { 68 | font-family: -apple-system, BlinkMacSystemFont, Segoe WPC, Segoe UI, 69 | HelveticaNeue-Light, Ubuntu, Droid Sans, sans-serif; 70 | 71 | .part-header { 72 | padding: 2px; 73 | padding-left: 5px; 74 | margin: 0; 75 | display: flex; 76 | align-items: center; 77 | 78 | .part-collapser { 79 | width: 10px; 80 | height: 10px; 81 | margin-right: 6px; 82 | } 83 | 84 | .part-text { 85 | cursor: pointer; 86 | span.style1 { 87 | color: var(--visualizer-tree-style1-foreground); 88 | } 89 | span.style2 { 90 | color: var(--visualizer-tree-style2-foreground); 91 | text-decoration: underline; 92 | padding: 1px 3px; 93 | margin-left: 5px; 94 | } 95 | span.style3 { 96 | color: var(--visualizer-tree-style3-foreground); 97 | border: var(--visualizer-tree-style3-border) solid 1px; 98 | padding: 1px 3px; 99 | margin-left: 5px; 100 | } 101 | span.unstyled { 102 | color: var(--visualizer-tree-unstyled-foreground); 103 | } 104 | } 105 | } 106 | 107 | .part-children { 108 | margin-left: 0px; 109 | margin-top: 0px; 110 | 111 | padding: 5px; 112 | padding-left: 15px; 113 | } 114 | 115 | &.selected { 116 | & > .part-header { 117 | font-weight: bold; 118 | } 119 | } 120 | &.hovered { 121 | & > .part-header > span { 122 | font-weight: bold; 123 | } 124 | & > .part-children { 125 | background: transparentize(gray, 0.8); 126 | } 127 | } 128 | } 129 | 130 | .component-TreeWithPathView { 131 | background-color: var(--visualizer-tree-background); 132 | 133 | display: flex; 134 | flex-direction: column; 135 | height: 100%; 136 | 137 | .part-path { 138 | overflow: hidden; 139 | white-space: pre; 140 | padding: 5px 10px; 141 | border-bottom: solid gray 1px; 142 | 143 | color: lightgray; 144 | 145 | .part-path-item { 146 | color: var(--visualizer-tree-path-foreground); 147 | margin-left: 1px; 148 | margin-right: 1px; 149 | } 150 | } 151 | .part-tree { 152 | overflow: auto; 153 | flex: 1; 154 | } 155 | } 156 | 157 | .decorator-collapse-icon { 158 | background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16'%3E%3Cpath fill='%23646465' d='M6 4v8l4-4-4-4zm1 2.414L8.586 8 7 9.586V6.414z'/%3E%3C/svg%3E"); 159 | 160 | background-position: 50%; 161 | background-position-x: 50%; 162 | background-position-y: center; 163 | background-repeat: no-repeat; 164 | 165 | &.expanded { 166 | background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16'%3E%3Cpath fill='%23646465' d='M11 10.07H5.344L11 4.414v5.656z'/%3E%3C/svg%3E"); 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /visualization-bundle/style.scss: -------------------------------------------------------------------------------- 1 | @import "./src/vars.scss"; 2 | @import "./src/visualizers/grid-visualizer/style.scss"; 3 | @import "./src/visualizers/simple-text-visualizer/style.scss"; 4 | @import "./src/visualizers/monaco-text-visualizer/style.scss"; 5 | @import "./src/visualizers/monaco-text-diff-visualizer/style.scss"; 6 | @import "./src/visualizers/tree-visualizer/style.scss"; 7 | @import "./src/visualizers/ast-visualizer/style.scss"; 8 | @import "./src/visualizers/svg-visualizer/style.scss"; 9 | @import "./src/visualizers/graph/graph-visjs-visualizer/style.scss"; 10 | @import "./src/visualizers/plotly-visualizer/style.scss"; 11 | -------------------------------------------------------------------------------- /visualization-bundle/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "CommonJS", 5 | "moduleResolution": "node", 6 | "strict": true, 7 | "outDir": "dist", 8 | "skipLibCheck": true, 9 | "newLine": "LF", 10 | "sourceMap": true, 11 | "jsx": "react", 12 | "experimentalDecorators": true, 13 | "declaration": true, 14 | "declarationMap": true, 15 | "allowSyntheticDefaultImports": true 16 | }, 17 | "include": ["src/**/*"] 18 | } 19 | -------------------------------------------------------------------------------- /visualization-core/README.md: -------------------------------------------------------------------------------- 1 | # Visualization Core 2 | 3 | ## Installation 4 | 5 | You can use yarn to install this library: 6 | 7 | ``` 8 | yarn add @hediet/visualization-core 9 | ``` 10 | 11 | You may want to install `@hediet/visualization-bundle` for the actual visualizations too! 12 | 13 | ## Usage 14 | 15 | Use `globalVisualizationFactory` to visualize some data: 16 | 17 | ```tsx 18 | import { globalVisualizationFactory } from "@hediet/visualization-core"; 19 | 20 | const data = { 21 | kind: { graph: true as true }, 22 | nodes: [ 23 | { id: "1", label: "1" }, 24 | { id: "2", label: "2", color: "orange" }, 25 | { id: "3", label: "3" }, 26 | ], 27 | edges: [ 28 | { from: "1", to: "2", color: "red" }, 29 | { from: "1", to: "3" }, 30 | ], 31 | }; 32 | 33 | const visualizations = globalVisualizationFactory.getVisualizations( 34 | data, 35 | /* preferred visualization id */ undefined 36 | ); 37 | // `visualizations.bestVisualization` is the visualization that is best suited to visualize the data. 38 | // `visualiztaions.allVisualizations` contains all suitable visualizations. 39 | ``` 40 | 41 | If you use react, you can use the `VisualizationView` component to render a visualization: 42 | 43 | ```tsx 44 | import { VisualizationView, Theme } from "@hediet/visualization-core"; 45 | 46 | // const visualizations = ... 47 | 48 | function App() { 49 | if (!visualizations.bestVisualization) { 50 | return null; 51 | } 52 | return ( 53 | 57 | ); 58 | } 59 | ``` 60 | 61 | ## Architecture 62 | 63 | ![](./architecture.drawio.svg) 64 | -------------------------------------------------------------------------------- /visualization-core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@hediet/visualization-core", 3 | "description": "Provides infrastructure to find registered visualizations for given json data", 4 | "version": "0.3.1", 5 | "license": "MIT", 6 | "homepage": "https://github.com/hediet/visualization", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/hediet/visualization.git" 10 | }, 11 | "author": { 12 | "name": "Henning Dieterichs", 13 | "email": "henning.dieterichs@live.de" 14 | }, 15 | "keywords": [ 16 | "visualization", 17 | "json" 18 | ], 19 | "files": [ 20 | "dist", 21 | "src" 22 | ], 23 | "scripts": { 24 | "dev": "tsc --watch", 25 | "build": "tsc" 26 | }, 27 | "main": "./dist/index.js", 28 | "types": "./dist/index.d.ts", 29 | "dependencies": { 30 | "@hediet/semantic-json": "^0.3.15", 31 | "@hediet/std": "^0.6.0", 32 | "mobx": "^5.15.4", 33 | "react": "^16.12.0", 34 | "react-dom": "^16.12.0", 35 | "react-measure": "^2.3.0" 36 | }, 37 | "devDependencies": { 38 | "@types/react": "^16.9.22", 39 | "@types/react-dom": "^16.9.5", 40 | "typescript": "^3.9.7" 41 | }, 42 | "publishConfig": { 43 | "access": "public", 44 | "registry": "https://registry.npmjs.org/" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /visualization-core/src/CanvasVisualization.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { createReactVisualization } from "./ReactVisualization"; 3 | import { Visualization, Visualizer } from "./Visualizer"; 4 | import { Disposable } from "@hediet/std/disposable"; 5 | 6 | export function createCanvas2DVisualization( 7 | sourceVisualizer: Visualizer, 8 | options: { priority: number }, 9 | render: (context: CanvasRenderingContext2D) => Disposable | void 10 | ): Visualization { 11 | return createReactVisualization( 12 | sourceVisualizer, 13 | { priority: options.priority }, 14 | () => ( 15 | { 17 | if (canvas) { 18 | const ctx = canvas.getContext("2d")!; 19 | ctx.clearRect(0, 0, canvas.width, canvas.height); 20 | 21 | // We don't want to use try/catch to swallow the error, so we use setTimeout to protect the caller 22 | setTimeout(() => { 23 | render(ctx); 24 | }, 0); 25 | } 26 | }} 27 | /> 28 | ) 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /visualization-core/src/ReactVisualization.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as ReactDom from "react-dom"; 3 | import { Visualization, Theme } from "./index"; 4 | import { 5 | Visualizer, 6 | VisualizationId, 7 | asVisualizationId, 8 | VisualizationRenderOptions, 9 | } from "./Visualizer"; 10 | import { Deferred } from "@hediet/std/synchronization"; 11 | 12 | interface ReactVisualizationArgs { 13 | priority: number; 14 | preload?: (() => Promise) | undefined; 15 | } 16 | 17 | class ReactVisualization implements Visualization { 18 | public readonly id: VisualizationId; 19 | public readonly name: string; 20 | public readonly priority: number; 21 | private readonly _preload: (() => Promise) | undefined; 22 | 23 | constructor( 24 | public readonly sourceVisualizer: Visualizer, 25 | options: ReactVisualizationArgs, 26 | private readonly getReactNode: ( 27 | options: ReactVisualizationRenderArgs 28 | ) => { 29 | node: React.ReactChild; 30 | ready: Promise; 31 | } 32 | ) { 33 | this.id = asVisualizationId(sourceVisualizer.id.toString()); 34 | this.name = sourceVisualizer.name; 35 | this.priority = options.priority; 36 | this._preload = options.preload; 37 | } 38 | 39 | public preload(): Promise { 40 | return this._preload ? this._preload() : Promise.resolve(); 41 | } 42 | 43 | public render( 44 | target: HTMLDivElement, 45 | options: VisualizationRenderOptions 46 | ): { renderState: unknown; ready: Promise } { 47 | const { node, ready } = this.getReactNode({ 48 | theme: options.theme, 49 | }); 50 | // We could use this to get access to theme vars: 51 | // getComputedStyle(target).getPropertyValue("--background-color") 52 | 53 | ReactDom.render(<>{node}, target); 54 | return { renderState: undefined, ready }; 55 | } 56 | } 57 | 58 | export interface ReactVisualizationRenderArgs { 59 | theme: Theme; 60 | } 61 | 62 | export function createReactVisualization( 63 | sourceVisualizer: Visualizer, 64 | args: ReactVisualizationArgs, 65 | renderReactNode: (args: ReactVisualizationRenderArgs) => React.ReactChild 66 | ) { 67 | return new ReactVisualization(sourceVisualizer, args, options => { 68 | const deferred = new Deferred(); 69 | return { 70 | node: ( 71 | deferred.resolve()} 73 | child={renderReactNode(options)} 74 | /> 75 | ), 76 | ready: deferred.promise, 77 | }; 78 | }); 79 | } 80 | 81 | class ComponentWithOnMount extends React.Component<{ 82 | child: React.ReactChild; 83 | onDidMount: () => void; 84 | }> { 85 | componentDidMount() { 86 | this.props.onDidMount(); 87 | } 88 | 89 | render(): React.ReactNode { 90 | return this.props.child; 91 | } 92 | } 93 | 94 | export function createLazyReactVisualization( 95 | sourceVisualizer: Visualizer, 96 | args: ReactVisualizationArgs, 97 | renderReactNode: ( 98 | args: ReactVisualizationRenderArgs 99 | ) => { 100 | node: React.ReactChild; 101 | ready: Promise; 102 | } 103 | ) { 104 | return new ReactVisualization(sourceVisualizer, args, renderReactNode); 105 | } 106 | -------------------------------------------------------------------------------- /visualization-core/src/RegisterVisualizerFn.ts: -------------------------------------------------------------------------------- 1 | import * as semanticJson from "@hediet/semantic-json"; 2 | import { createCanvas2DVisualization } from "./CanvasVisualization"; 3 | import { CreateVisualizerOptions } from "./createVisualizer"; 4 | import { Visualization, Visualizer } from "./Visualizer"; 5 | 6 | export type RegisterVisualizerFn = ( 7 | register: ( 8 | visualizationOptions: CreateVisualizerOptions 9 | ) => void, 10 | lib: Lib 11 | ) => void; 12 | 13 | interface Lib { 14 | semanticJson: typeof semanticJson; 15 | 16 | createCanvas2DVisualization: ( 17 | visualizer: Visualizer, 18 | options: { priority: number }, 19 | render: (context: CanvasRenderingContext2D) => void 20 | ) => Visualization; 21 | } 22 | 23 | export const libImplementation: Lib = { 24 | semanticJson, 25 | createCanvas2DVisualization, 26 | }; 27 | -------------------------------------------------------------------------------- /visualization-core/src/Theme.ts: -------------------------------------------------------------------------------- 1 | export class Theme { 2 | private static _light: Theme; 3 | public static get light(): Theme { 4 | if (!this._light) { 5 | this._light = new Theme("light", "light"); 6 | } 7 | return this._light; 8 | } 9 | 10 | private static _dark: Theme; 11 | public static get dark(): Theme { 12 | if (!this._dark) { 13 | this._dark = new Theme("dark", "dark"); 14 | } 15 | return this._dark; 16 | } 17 | 18 | private readonly divElement = document.createElement("div"); 19 | 20 | constructor( 21 | public readonly id: string, 22 | public readonly kind: "light" | "dark" 23 | ) { 24 | this.divElement.classList.add( 25 | "themeable", 26 | `theme-${this.id}`, 27 | "visualization" 28 | ); 29 | this.divElement.style.display = "none"; 30 | document.body.append(this.divElement); 31 | } 32 | 33 | public resolveVarToColor(varName: string): string { 34 | return getComputedStyle(this.divElement).getPropertyValue(varName); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /visualization-core/src/VisualizationData.ts: -------------------------------------------------------------------------------- 1 | export interface VisualizationData { 2 | kind: Record; 3 | } 4 | 5 | export function isVisualizationData(val: unknown): val is VisualizationData { 6 | if (typeof val !== "object" || !val || !("kind" in val)) { 7 | return false; 8 | } 9 | 10 | const obj = val as any; 11 | if (typeof obj.kind !== "object" || !obj.kind) { 12 | return false; 13 | } 14 | 15 | return Object.values(obj.kind).every(val => val === true); 16 | } 17 | -------------------------------------------------------------------------------- /visualization-core/src/VisualizationView.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Visualization } from "./Visualizer"; 3 | import { Theme } from "./Theme"; 4 | 5 | export class VisualizationView extends React.Component<{ 6 | visualization: Visualization; 7 | theme: Theme; 8 | onReady?: () => void; 9 | }> { 10 | private readonly ref = React.createRef(); 11 | private visualizationRenderState: unknown = undefined; 12 | 13 | componentDidMount() { 14 | this.renderVisualization(); 15 | } 16 | 17 | componentDidUpdate() { 18 | this.renderVisualization(); 19 | } 20 | 21 | renderVisualization() { 22 | const { visualization, theme } = this.props; 23 | const { renderState, ready } = visualization.render(this.ref.current!, { 24 | theme, 25 | previousRenderState: this.visualizationRenderState, 26 | }); 27 | 28 | ready.then(() => { 29 | if (this.props.onReady) { 30 | return this.props.onReady(); 31 | } 32 | }); 33 | 34 | this.visualizationRenderState = renderState; 35 | } 36 | 37 | render() { 38 | return ( 39 |
48 | ); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /visualization-core/src/Visualizer.ts: -------------------------------------------------------------------------------- 1 | import { BaseSerializer } from "@hediet/semantic-json"; 2 | import { Theme } from "./Theme"; 3 | 4 | export interface Visualizer { 5 | readonly id: VisualizerId; 6 | readonly name: string; 7 | 8 | /** 9 | * Allows to deserialize JSON into an visualization. 10 | * This object is also used to inspect the schema of that JSON. 11 | */ 12 | readonly serializer: BaseSerializer; 13 | } 14 | 15 | export interface VisualizerId extends String { 16 | __brand: "VisualizerId"; 17 | } 18 | 19 | export function asVisualizerId(id: string): VisualizerId { 20 | return id as unknown as VisualizerId; 21 | } 22 | 23 | export interface Visualization { 24 | readonly id: VisualizationId; 25 | readonly name: string; 26 | readonly priority: number; 27 | 28 | render( 29 | target: HTMLDivElement, 30 | options: VisualizationRenderOptions 31 | ): { 32 | renderState: TRenderState; 33 | /** 34 | * Is resolved when the visualization stabilized and is ready to be shown. 35 | * Note that the visualization can be shown before this promise resolves. 36 | * This promise is useful for loading animations or integrations. 37 | */ 38 | ready: Promise; 39 | }; 40 | 41 | /** 42 | * Preloads resources to speed up first rendering. 43 | * The visualization does not need to be rendered for this. 44 | */ 45 | preload(): Promise; 46 | } 47 | 48 | export interface VisualizationId extends String { 49 | __brand: "VisualizationId"; 50 | } 51 | 52 | export function asVisualizationId(id: string): VisualizationId { 53 | return id as unknown as VisualizationId; 54 | } 55 | 56 | export interface VisualizationRenderOptions { 57 | theme: Theme; 58 | previousRenderState: TRenderState | undefined; 59 | } 60 | -------------------------------------------------------------------------------- /visualization-core/src/VisualizerRegistry.ts: -------------------------------------------------------------------------------- 1 | import { Visualizer, Visualization, VisualizationId } from "./Visualizer"; 2 | import { VisualizationData } from "./VisualizationData"; 3 | import { JSONValue, Serializer, sUnionMany } from "@hediet/semantic-json"; 4 | import { ObservableMap } from "mobx"; 5 | 6 | export class VisualizationFactory { 7 | private readonly visualizers = new ObservableMap(); 8 | private readonly hiddenVisualizers = new ObservableMap< 9 | string, 10 | Visualizer 11 | >(); 12 | 13 | public getSerializer(): Serializer<[Visualization, Visualizer][]> { 14 | return this._getSerializer(this.visualizers); 15 | } 16 | 17 | private _getSerializer(map: ObservableMap): Serializer<[Visualization, Visualizer][]> { 18 | return sUnionMany( 19 | [...map.values()].map((v) => v.serializer.asSerializer().refine<[Visualization, Visualizer]>({ 20 | canSerialize: (v): v is [Visualization, Visualizer] => false, 21 | fromIntermediate: value => [value, v], 22 | toIntermediate: () => { throw new Error("not supported"); } 23 | })), 24 | { processingStrategy: "all" } 25 | ); 26 | } 27 | 28 | public addVisualizer(visualizer: Visualizer): void { 29 | this.visualizers.set(visualizer.id.toString(), visualizer); 30 | } 31 | 32 | public getRegisteredVisualizers(): Visualizer[] { 33 | return [...this.visualizers.values()]; 34 | } 35 | 36 | public getRegisteredHiddenVisualizer(): Visualizer[] { 37 | return [...this.hiddenVisualizers.values()]; 38 | } 39 | 40 | /** 41 | * Hidden visualizers don't appear in `getSerializer` and should only be presented to the user if he explicitly selects it. 42 | * `getVisualizations(...).bestVisualization` is only the result of a hidden visualizer if the user requests a hidden visualization id. 43 | */ 44 | public addHiddenVisualizer(visualizer: Visualizer): void { 45 | this.hiddenVisualizers.set(visualizer.id.toString(), visualizer); 46 | } 47 | 48 | public getVisualizations( 49 | data: VisualizationData, 50 | preferredVisualization: VisualizationId | undefined 51 | ): Visualizations { 52 | const u = this.getSerializer(); 53 | const result = u.deserialize(data as unknown as JSONValue); 54 | const allVisualizations = result.value || []; 55 | 56 | const u2 = this._getSerializer(this.hiddenVisualizers); 57 | const hiddenResult = u2.deserialize(data as unknown as JSONValue); 58 | allVisualizations.push(...(hiddenResult.value || [])); 59 | 60 | allVisualizations.sort(([a], [b]) => b.priority - a.priority); 61 | 62 | let bestVisualization: [Visualization, Visualizer] | undefined = allVisualizations[0]; 63 | if (bestVisualization && bestVisualization[0].priority < 0) { 64 | bestVisualization = undefined; 65 | } 66 | if (preferredVisualization) { 67 | const preferred = allVisualizations.find( 68 | ([vis]) => vis.id === preferredVisualization 69 | ); 70 | if (preferred) { 71 | bestVisualization = preferred; 72 | } 73 | } 74 | 75 | return { 76 | bestVisualization: bestVisualization ? bestVisualization[0] : undefined, 77 | bestVisualizationVisualizer: bestVisualization ? bestVisualization[1] : undefined, 78 | allVisualizations: allVisualizations.map(v => v[0]), 79 | visualizationDataErrors: [], 80 | }; 81 | } 82 | } 83 | 84 | export interface Visualizations { 85 | bestVisualization: Visualization | undefined; 86 | bestVisualizationVisualizer: Visualizer | undefined; 87 | allVisualizations: Visualization[]; 88 | visualizationDataErrors: VisualizationDataError[]; 89 | } 90 | 91 | export interface VisualizationDataError { 92 | visualizer: Visualizer; 93 | message: string; 94 | } 95 | 96 | export const globalVisualizationFactory = new VisualizationFactory(); 97 | -------------------------------------------------------------------------------- /visualization-core/src/createVisualizer.ts: -------------------------------------------------------------------------------- 1 | import { BaseSerializer } from "@hediet/semantic-json"; 2 | import { Visualization, Visualizer, asVisualizerId } from "./Visualizer"; 3 | 4 | export interface CreateVisualizerOptions { 5 | /** 6 | * A unique id for this visualizer. 7 | */ 8 | id: string; 9 | 10 | /** 11 | * A human readable name of this visualizer. 12 | * Should be in English language. 13 | */ 14 | name: string; 15 | 16 | serializer: BaseSerializer; 17 | getVisualization: (data: TData, self: Visualizer) => Visualization; 18 | } 19 | 20 | export function createVisualizer( 21 | options: CreateVisualizerOptions 22 | ): Visualizer { 23 | const result: Visualizer = { 24 | id: asVisualizerId(options.id), 25 | name: options.name, 26 | serializer: options.serializer.refine({ 27 | fromIntermediate: (data) => options.getVisualization(data, result), 28 | canSerialize: (val): val is Visualization => false, 29 | toIntermediate: () => { 30 | throw new Error("not supported"); 31 | }, 32 | }), 33 | }; 34 | return result; 35 | } 36 | -------------------------------------------------------------------------------- /visualization-core/src/index.tsx: -------------------------------------------------------------------------------- 1 | export * from "./VisualizationData"; 2 | export * from "./ReactVisualization"; 3 | export * from "./Visualizer"; 4 | export * from "./VisualizerRegistry"; 5 | export * from "./VisualizationView"; 6 | export * from "./Theme"; 7 | export * from "./createVisualizer"; 8 | export * from "./RegisterVisualizerFn"; 9 | -------------------------------------------------------------------------------- /visualization-core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "CommonJS", 5 | "moduleResolution": "node", 6 | "strict": true, 7 | "outDir": "dist", 8 | "skipLibCheck": true, 9 | "newLine": "LF", 10 | "sourceMap": true, 11 | "jsx": "react", 12 | "experimentalDecorators": true, 13 | "declaration": true, 14 | "declarationMap": true, 15 | "allowSyntheticDefaultImports": true 16 | }, 17 | "include": ["src/**/*"] 18 | } 19 | --------------------------------------------------------------------------------