58 Commits

Author SHA1 Message Date
Henning Dieterichs
9e6dc9a5ad version bump 2024-01-04 19:18:26 +01:00
Henning Dieterichs
21fa200446 Update version 2023-11-19 14:03:31 +01:00
Henning Dieterichs
5b0d979a23 Adds border color 2023-11-19 14:03:11 +01:00
Henning Dieterichs
96115a9a70 Adds support for external visualizer functions. 2023-04-21 11:52:30 +02:00
Henning Dieterichs
ac15ceb29c Updates visualization-bundle version to 0.2.4 2022-03-22 16:15:50 +01:00
Henning Dieterichs
f089760a72 Merge pull request #8 from fehrlich/plotly-multiview
add domain for plotly
2021-08-11 13:01:21 +02:00
fehrlich
1990749b4a add domain for plotly 2021-08-11 12:50:24 +02:00
Henning Dieterichs
0460009bd2 Merge pull request #7 from fehrlich/plotly-tables
Plotly tables
2021-08-11 12:40:49 +02:00
fehrlich
ffcd3c9ad1 fixed commit 2021-08-11 12:09:01 +02:00
fehrlich
97431a5c89 added plotly tableview 2021-08-11 12:06:35 +02:00
Henning Dieterichs
d8e18743c0 Merge pull request #6 from paulshen/fix-mobx-dep
Fix mobx dependency
2021-06-29 18:54:10 +02:00
Paul Shen
8e6c7f3c6f Fix mobx dependency 2021-06-29 09:05:01 -07:00
Henning Dieterichs
4be4717e61 Merge pull request #5 from paulshen/add-mobx-dep
Add mobx as explicit dependency
2021-06-29 09:07:52 +02:00
Paul Shen
2fcb84a84d Add mobx as explicit dependency 2021-06-28 14:44:39 -07:00
Henning Dieterichs
fdd4927a50 Instructs monaco editor to define the global monaco object. 2021-06-19 16:30:12 +02:00
Henning Dieterichs
1d0e559eb3 Enable all visualizations. 2021-06-19 16:11:45 +02:00
Henning Dieterichs
188d10128a Updates monaco-editor-webpack-plugin. 2021-06-19 15:41:15 +02:00
Henning Dieterichs
4cab6cc1b5 v0.2.3. Updates monaco to 0.25.2, adds json schema validation. 2021-06-19 15:35:33 +02:00
Henning Dieterichs
b6bf5feee0 Don't ignore trim whitespace when diffing strings. 2021-06-19 15:34:28 +02:00
Henning Dieterichs
7576ed1b7d Adds links. 2021-06-07 01:10:28 +02:00
Henning Dieterichs
f337e22a64 Makes themes lazy so they can load on nodejs. 2021-06-07 01:07:35 +02:00
Henning Dieterichs
7310851d2b v0.2.1 2021-06-07 00:46:18 +02:00
Henning Dieterichs
814b33720d Renders all whitespace for text visualization & introduces decoration support. 2021-06-07 00:43:03 +02:00
Henning Dieterichs
ec221b506e v0.2.0 2020-10-08 10:51:12 +02:00
Henning Dieterichs
4029023074 v0.2.0 2020-10-08 10:50:35 +02:00
Henning Dieterichs
cf382c71c9 Improves lazy loading. 2020-10-08 10:50:19 +02:00
Henning Dieterichs
5512c0b244 Adds style property to graph edges. 2020-09-30 17:45:15 +02:00
Henning Dieterichs
b358a5a7b5 v0.1.8 2020-09-28 12:03:57 +02:00
Henning Dieterichs
810e100e66 Fixes theme background colors. 2020-09-28 11:48:17 +02:00
Henning Dieterichs
16440b39c5 Playground publish script. 2020-09-23 13:03:33 +02:00
Henning Dieterichs
3ff96aff56 @hediet/visualization-bundle-v0.1.7 2020-09-23 13:02:17 +02:00
Henning Dieterichs
107ba3f8e8 Updates playground demos. 2020-09-23 12:29:20 +02:00
Henning Dieterichs
cd9f7aa5ef Updates readme, improves text for how to implement new visualizations. 2020-09-23 12:28:47 +02:00
Henning Dieterichs
27a616d43f Improves plotly mesh3d visualization by removing unnecessary margin. 2020-09-23 12:28:10 +02:00
Henning Dieterichs
c9143877f7 Configures format on save for VS Code. 2020-09-23 12:27:36 +02:00
Henning Dieterichs
7f158bd500 Fixes out of heap build issue for simple-demo. 2020-08-29 18:22:09 +02:00
Henning Dieterichs
340a5a5315 sets up github build action. 2020-08-29 17:41:53 +02:00
Henning Dieterichs
aa2502452e Configures sass dependency 2020-08-29 17:41:39 +02:00
Henning Dieterichs
9277f7a99a Updates readme. 2020-08-29 13:50:49 +02:00
Henning Dieterichs
620458924b Adds architecture. 2020-08-29 13:45:58 +02:00
Henning Dieterichs
3cdbd1d9b1 Improves docs. 2020-08-29 13:44:40 +02:00
Henning Dieterichs
2c8ac3977c Increases priority of the diff visualizer. 2020-08-29 13:44:25 +02:00
Henning Dieterichs
4a564eca9b Adds diff visualizer. 2020-08-29 12:45:27 +02:00
Henning Dieterichs
33de54d696 Patch version update. 2020-08-23 18:07:15 +02:00
Henning Dieterichs
3a64fd9704 Adds AST demo to playground. 2020-08-23 18:07:01 +02:00
Henning Dieterichs
6bdf8c3b61 Fixes tree path display. 2020-08-23 18:06:49 +02:00
Henning Dieterichs
84d59393ee Fixes line column import. 2020-08-23 18:06:30 +02:00
Henning Dieterichs
743d0db6c9 Improves build script. 2020-08-23 16:47:09 +02:00
Henning Dieterichs
f7fa31995e Updates bundle version to v0.1.4. 2020-08-23 16:47:03 +02:00
Henning Dieterichs
20c52ff4a7 Fixes bug in perspective viewer that occurs when schema of data changes. 2020-08-23 16:37:40 +02:00
Henning Dieterichs
8b6d65614d Improvements. 2020-08-22 20:01:25 +02:00
Henning Dieterichs
1fe6ae1270 Updates bundle version. 2020-08-22 14:19:38 +02:00
Henning Dieterichs
1911a8092e Improves visualization data serializers and implements typescript declaration file extraction. 2020-08-22 14:12:57 +02:00
Henning Dieterichs
b46c7155b2 Removes vs code style reference. 2020-08-16 11:02:26 +02:00
Henning Dieterichs
3c41b3c0bf Disables static port hosting for the playground. 2020-08-16 11:02:07 +02:00
Henning Dieterichs
c0e10d9a97 Improves serializer documentation. 2020-08-16 11:01:53 +02:00
Henning Dieterichs
41357e6821 Improves readme, removes blueprint from bundle package, some minor changes. 2020-08-14 13:48:39 +02:00
Henning Dieterichs
93ea01fc79 Updates playground title. 2020-08-13 20:05:02 +02:00
76 changed files with 2988 additions and 1899 deletions

23
.github/workflows/ci.yml vendored Normal file
View File

@@ -0,0 +1,23 @@
on:
push:
branches:
- master
pull_request:
jobs:
build:
strategy:
matrix:
os: [macos-latest, ubuntu-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- name: Checkout
uses: actions/checkout@v2
with:
submodules: true
- name: Install Node.js
uses: actions/setup-node@v1
with:
node-version: 10.x
- run: yarn install
- run: yarn build

26
.github/workflows/playground.yml vendored Normal file
View File

@@ -0,0 +1,26 @@
name: Release
on:
push:
branches:
- master
jobs:
release:
name: Release
runs-on: ubuntu-18.04
steps:
- name: Checkout
uses: actions/checkout@v1
- name: Setup Node.js
uses: actions/setup-node@v1
with:
node-version: 12
- name: Install dependencies
run: yarn install
- name: Build
run: yarn build
- name: Publish github pages
run: |
git remote set-url origin https://git:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git
yarn workspace playground run pub
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -1,3 +1,5 @@
{
"tasksStatusbar.taskLabelFilter": "dev"
"tasksStatusbar.taskLabelFilter": "dev",
"editor.formatOnSave": true,
"typescript.tsdk": "node_modules\\typescript\\lib"
}

View File

@@ -1,76 +1,16 @@
# Visualization Framework
## Installation
This repository contains two packages.
You can use yarn to install the core library:
## @hediet/visualization-core
```
yarn add @hediet/visualization-core
```
This package provides basic infrastructure for implementing visualizations.
See its readme [here](./visualization-core/README.md).
If you want to use the existing visualizations, add the bundle library:
## @hediet/visualization-bundle
```
yarn add @hediet/visualization-bundle
```
This package bundles all the visualizations.
See its readme [here](./visualization-bundle/README.md).
## Usage
Import `@hediet/visualization-bundle` to register all default visualizations:
```tsx
// This registers all visualizations
import "@hediet/visualization-bundle";
```
Don't forget to import the styles in your scss file:
```scss
@import "~@hediet/visualization-bundle/style.scss";
```
Use `globalVisualizationFactory` to visualize some data:
```tsx
import { globalVisualizationFactory } from "@hediet/visualization-core";
const data = {
kind: { graph: true as true },
nodes: [
{ id: "1", label: "1" },
{ id: "2", label: "2", color: "orange" },
{ id: "3", label: "3" },
],
edges: [
{ from: "1", to: "2", color: "red" },
{ from: "1", to: "3" },
],
};
const visualizations = globalVisualizationFactory.getVisualizations(
data,
/* preferred visualization id */ undefined
);
// `visualizations.bestVisualization` is the visualization that is best suited to visualize the data.
// `visualiztaions.allVisualizations` contains all other available visualizations than can visualize the data.
```
If you use react, you can use the `VisualizationView` component to render a visualization:
```tsx
import { VisualizationView, Theme } from "@hediet/visualization-core";
// const visualizations = ...
function App() {
if (!visualizations.bestVisualization) {
return null;
}
return (
<VisualizationView
theme={Theme.light}
visualization={visualizations.bestVisualization}
/>
);
}
```
If you want to add your own visualization to this project, visualization-bundle is the right place!
The readme documents how to implement your own visualization.

View File

@@ -6,7 +6,13 @@
"./simple-demo",
"./playground"
],
"scripts": {},
"scripts": {
"build": "yarn build-core && yarn build-bundle && yarn build-playground && yarn build-simple-demo",
"build-core": "yarn workspace @hediet/visualization-core run build",
"build-bundle": "yarn workspace @hediet/visualization-bundle run build",
"build-playground": "yarn workspace playground run build",
"build-simple-demo": "yarn workspace simple-demo run build"
},
"devDependencies": {
"prettier": "^1.19.1"
}

View File

@@ -3,25 +3,27 @@
"version": "0.0.1",
"license": "MIT",
"scripts": {
"dev": "webpack-dev-server --hot --port 8080",
"build": "webpack",
"pub": "gh-pages -d dist"
"dev": "webpack-dev-server --hot",
"build": "yarn build-webpack && yarn build-docs",
"build-webpack": "node --max_old_space_size=4096 --openssl-legacy-provider ../node_modules/webpack/bin/webpack.js --mode production",
"build-docs": "ts-node ./scripts/build-docs",
"pub": "gh-pages -d dist -u 'github-actions-bot <support+actions@github.com>'"
},
"dependencies": {
"@blueprintjs/core": "^3.23.1",
"@hediet/std": "^0.6.0",
"@hediet/semantic-json": "^0.3.11",
"@hediet/semantic-json": "^0.3.14",
"blueprintjs": "^0.0.8",
"classnames": "^2.2.6",
"mobx": "^5.15.4",
"mobx-react": "^6.1.8",
"monaco-editor": "^0.20.0",
"monaco-editor": "^0.25.2",
"react": "^16.12.0",
"react-dom": "^16.12.0",
"messagepack": "^1.1.10",
"base64-js": "^1.3.1",
"lzma": "^2.3.2",
"@hediet/monaco-editor-react": "^0.1.0"
"@hediet/monaco-editor-react": "^0.2.0"
},
"devDependencies": {
"gh-pages": "^2.2.0",
@@ -35,9 +37,10 @@
"file-loader": "^5.1.0",
"fork-ts-checker-webpack-plugin": "^4.0.4",
"html-webpack-plugin": "^3.2.0",
"monaco-editor-webpack-plugin": "^1.9.0",
"monaco-editor-webpack-plugin": "^4.0.0",
"raw-loader": "^4.0.0",
"sass-loader": "^8.0.2",
"sass": "^1.25.0",
"style-loader": "^1.1.3",
"ts-loader": "^6.2.1",
"ts-node": "^8.6.2",

View File

@@ -0,0 +1,34 @@
import {
JsonSchemaGenerator,
TypeScriptTypeGenerator,
} from "@hediet/semantic-json";
import { writeFileSync, mkdirSync } from "fs";
import { join } from "path";
import { globalVisualizationFactory } from "@hediet/visualization-core";
import "../src/visualizations";
const tsGen = new TypeScriptTypeGenerator();
const tp = tsGen.getType(globalVisualizationFactory.getSerializer());
const tsSrc = `export type KnownVisualizationData = ${tp};\n\n\n${[
...tsGen.definitions.values(),
]
.map(v => v.getDefinitionSource({ exported: true }) + `\n\n`)
.join("")}`;
const targetDir = join(__dirname, "../dist/docs");
mkdirSync(targetDir, { recursive: true });
writeFileSync(join(targetDir, "visualization-data.ts"), tsSrc, {
encoding: "utf-8",
});
const schema = new JsonSchemaGenerator().getJsonSchemaWithDefinitions(
globalVisualizationFactory.getSerializer()
);
const jsonSchema = JSON.stringify(schema, undefined, 4);
writeFileSync(join(targetDir, "visualization-data-schema.json"), jsonSchema, {
encoding: "utf-8",
});

View File

@@ -174,7 +174,7 @@ class VisualizationConfigView extends React.Component<{
if (visualizations.bestVisualization) {
const tsTypeGenerator = new TypeScriptTypeGenerator();
const typeSrc = tsTypeGenerator.getType(
visualizations.bestVisualization.sourceVisualizer.serializer.asSerializer()
visualizations.bestVisualizationVisualizer!.serializer.asSerializer()
);
const decl = tsTypeGenerator.getDefinitionSource();
return {

File diff suppressed because one or more lines are too long

View File

@@ -1,3 +1,5 @@
(globalThis as any).MonacoEnvironment = { globalAPI: true };
import * as React from "react";
import * as ReactDOM from "react-dom";
import "./style.scss";

View File

@@ -61,10 +61,7 @@ body {
@import "./vscode-dark.scss";
@import "./vscode-light.scss";
.visualization {
background: var(--vscode-editor-background);
}
.themeable {
background-color: violet; // To debug backgrounds
--visualizer-simple-text-color: var(--vscode-editor-foreground);
}

View File

@@ -1,21 +1,27 @@
//// Uncomment the visualization that you work on to increase webpack build speed!
// import "@hediet/visualization-bundle/dist/visualizers/ast-visualizer"; /*
// import "@hediet/visualization-bundle/dist/visualizers/graph/dot-graphviz-visualizer" /*
// import "@hediet/visualization-bundle/dist/visualizers/graph/graph-graphviz-visualizer" /*
// import "@hediet/visualization-bundle/dist/visualizers/graph/graph-visjs-visualizer" /*
// import "@hediet/visualization-bundle/dist/visualizers/grid-visualizer" /*
// import "@hediet/visualization-bundle/dist/visualizers/image-visualizer"; /*
// import "@hediet/visualization-bundle/dist/visualizers/monaco-text-visualizer" /*
// import "@hediet/visualization-bundle/dist/visualizers/perspective-table-visualizer" /*
// import "@hediet/visualization-bundle/dist/visualizers/plotly-visualizer" /*
// import "@hediet/visualization-bundle/dist/visualizers/simple-text-visualizer"; /*
// import "@hediet/visualization-bundle/dist/visualizers/source-visualizer" /*
// import "@hediet/visualization-bundle/dist/visualizers/svg-visualizer" /*
// import "@hediet/visualization-bundle/dist/visualizers/tree-visualizer"; /*
// This bundles the monaco editor. Uncomment it to load monaco dynamically.
// Dynamic loading increases webpack build speed significantly.
import "monaco-editor";
import "@hediet/visualization-bundle"; // */
//// Uncomment the visualization that you work on to increase webpack build speed!
//// If you create a new visualization, add it here!
// import "@hediet/visualization-bundle/dist/visualizers/ast-visualizer"; /*
// import "@hediet/visualization-bundle/dist/visualizers/graph/dot-graphviz-visualizer" /*
// import "@hediet/visualization-bundle/dist/visualizers/graph/graph-graphviz-visualizer" /*
// import "@hediet/visualization-bundle/dist/visualizers/graph/graph-visjs-visualizer" /*
// import "@hediet/visualization-bundle/dist/visualizers/grid-visualizer" /*
// import "@hediet/visualization-bundle/dist/visualizers/image-visualizer"; /*
// import "@hediet/visualization-bundle/dist/visualizers/monaco-text-visualizer" /*
// import "@hediet/visualization-bundle/dist/visualizers/monaco-text-diff-visualizer"; /*
// import "@hediet/visualization-bundle/dist/visualizers/perspective-table-visualizer" /*
// import "@hediet/visualization-bundle/dist/visualizers/plotly-visualizer" /*
// import "@hediet/visualization-bundle/dist/visualizers/simple-text-visualizer"; /*
// import "@hediet/visualization-bundle/dist/visualizers/source-visualizer"; /*
// import "@hediet/visualization-bundle/dist/visualizers/svg-visualizer" /*
// import "@hediet/visualization-bundle/dist/visualizers/tree-visualizer"; /*
// This bundles the monaco editor. Uncomment it to load monaco dynamically.
// Dynamic loading increases webpack build speed significantly.
if (typeof process === undefined) {
// We check for process so that we don't load monaco from nodejs.
require("monaco-editor");
}
// This import bundles *all* visualizations. This makes webpack super slow.
import "@hediet/visualization-bundle"; // */

View File

@@ -56,7 +56,7 @@ module.exports = {
plugins: (() => {
const plugins: any[] = [
new HtmlWebpackPlugin({
title: "Debug Visualizer",
title: "Visualization Playground",
}),
new ForkTsCheckerWebpackPlugin(),
new CleanWebpackPlugin(),
@@ -64,7 +64,6 @@ module.exports = {
plugins.push(
new MonacoWebpackPlugin({
// Add more languages here once webworker issues are solved.
languages: ["typescript", "json"],
})
);

View File

@@ -4,7 +4,7 @@
"license": "MIT",
"scripts": {
"dev": "webpack-dev-server --hot --port 8090",
"build": "webpack"
"build": "node --max_old_space_size=4096 --openssl-legacy-provider ../node_modules/webpack/bin/webpack.js --mode production"
},
"dependencies": {
"@hediet/visualization-core": "^0.1.0",
@@ -22,9 +22,10 @@
"file-loader": "^5.1.0",
"fork-ts-checker-webpack-plugin": "^4.0.4",
"html-webpack-plugin": "^3.2.0",
"monaco-editor-webpack-plugin": "^1.9.0",
"monaco-editor-webpack-plugin": "^4.0.0",
"raw-loader": "^4.0.0",
"sass-loader": "^8.0.2",
"sass": "^1.25.0",
"style-loader": "^1.1.3",
"ts-loader": "^6.2.1",
"ts-node": "^8.6.2",

View File

@@ -64,7 +64,6 @@ module.exports = {
plugins.push(
new MonacoWebpackPlugin({
// Add more languages here once webworker issues are solved.
languages: ["typescript", "json"],
})
);

View File

@@ -1,11 +1,17 @@
# Visualization Bundle
This package registers various visualizations. If you have an idea for a cool visualization, add it here!
## Installation
You can use yarn to install this library:
```
yarn add @hediet/visualization-bundle
```
Install `@hediet/visualization-core` before!
## Usage
Import `@hediet/visualization-bundle` to register all default visualizations to `globalVisualizationFactory` of `@hediet/visualization-core`:
@@ -21,11 +27,38 @@ Don't forget to import the styles in your scss file:
@import "~@hediet/visualization-bundle/style.scss";
```
You will need to configure webpack
- to compile scss files:
```js
{ test: /\.scss$/, loader: "style-loader!css-loader!sass-loader" },
```
- to compile less files:
```js
{ test: /\.less$/, loaders: ["style-loader", "css-loader", "less-loader"] }
```
- to register monaco workers
```js
plugins: [
// ...
new MonacoWebpackPlugin({
languages: ["typescript", "json"],
}),
];
```
See docs of `@hediet/visualization-core` for how to find and render visualizations!
## Supported Visualizations
### Origin Visualizations
You can find a playground that demonstrates all visualizations [here](https://hediet.github.io/visualization/).
### Source Visualizations
- Tree Visualizer
- AST Visualizer (+ Monaco Source Code View)
@@ -34,7 +67,7 @@ See docs of `@hediet/visualization-core` for how to find and render visualizatio
- Image Visualizer
- Simple Text Visualizer
### Integrated Visualizations
### Integrated Visualization Libraries
- Plotly Visualizer
- Perspective JS Visualizer
@@ -43,3 +76,27 @@ See docs of `@hediet/visualization-core` for how to find and render visualizatio
- Graphviz Dot Visualizer
- Vis.js Visualizer
- Monaco Editor Source Code Visualizer
## Setup Local Development Copy
- Clone this repository.
- Run `yarn` in the root folder.
- Run `yarn dev` in the `visualization-bundle` folder to start tsc in watch mode.
- Edit [./playground/src/visualization.ts](../playground/src/visualizations.ts) to your needs to improve webpack performance. This is highly recommended, but not required.
- Run `yarn dev` in the `playground` folder to start the playground where you can debug your visualization.
Any changes to the source are now reflected in the playground!
## Implementing New Visualizations
First, setup your local development copy.
You can use the [`simple-text-visualizer`](./src/visualizers/simple-text-visualizer/index.tsx) as starting point!
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!
Use the "open" button in the playground to debug your new visualization!
## Machine Readable Documents
- [TypeScript Declarations](https://hediet.github.io/visualization/docs/visualization-data.ts)
- [JSON Schema](https://hediet.github.io/visualization/docs/visualization-data-schema.json)

View File

@@ -1,7 +1,7 @@
{
"name": "@hediet/visualization-bundle",
"description": "Bundles several visualizers",
"version": "0.1.0",
"version": "0.2.5",
"license": "MIT",
"scripts": {
"dev": "tsc --watch",
@@ -14,46 +14,43 @@
},
"files": [
"dist",
"src"
"src",
"style.scss"
],
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"dependencies": {
"@hediet/std": "^0.6.0",
"@hediet/semantic-json": "^0.3.11",
"classnames": "^2.2.6",
"mobx": "^5.15.4",
"mobx-react": "^6.1.8",
"monaco-editor": "^0.20.0",
"react": "^16.12.0",
"react-dom": "^16.12.0",
"react-measure": "^2.3.0",
"react-svg-pan-zoom": "^3.8.0",
"line-column": "^1.0.2",
"vis-network": "^8.0.0",
"vis-data": "^7.0.0",
"viz.js": "^2.1.2",
"react-plotly.js": "^2.4.0",
"plotly.js": "^1.54.7",
"@finos/perspective-viewer": "^0.5.2",
"@finos/perspective-viewer-d3fc": "^0.5.2",
"@finos/perspective-viewer-datagrid": "^0.5.2",
"@hediet/monaco-editor-react": "^0.1.0"
"@hediet/monaco-editor-react": "^0.2.0",
"@hediet/semantic-json": "^0.3.15",
"@hediet/std": "^0.6.0",
"classnames": "^2.2.6",
"line-column": "^1.0.2",
"mobx": "^5.15.4",
"mobx-react": "^6.1.8",
"monaco-editor": "^0.25.2",
"plotly.js": "^1.54.7",
"react": "^16.12.0",
"react-dom": "^16.12.0",
"react-measure": "^2.3.0",
"react-plotly.js": "^2.4.0",
"react-svg-pan-zoom": "^3.8.0",
"vis-data": "^7.0.0",
"vis-network": "^8.0.0",
"viz.js": "^2.1.2"
},
"peerDependencies": {
"@hediet/visualization-core": "^0.1.0"
"@hediet/visualization-core": "0.*"
},
"devDependencies": {
"@types/plotly.js": "^1.50.16",
"@types/react-plotly.js": "^2.2.4",
"@types/react-measure": "^2.0.6",
"@types/react-svg-pan-zoom": "^3.3.0",
"@types/webpack": "^4.41.6",
"@types/classnames": "^2.2.9",
"@types/line-column": "^1.0.0",
"@types/plotly.js": "^1.50.16",
"@types/react": "^16.9.22",
"@types/react-dom": "^16.9.5",
"@types/line-column": "^1.0.0",
"sass": "^1.25.0",
"@types/webpack": "^4.41.6",
"ts-loader": "^6.2.1",
"ts-node": "^8.6.2",
"typescript": "^3.9.7"

18
visualization-bundle/src/types.d.ts vendored Normal file
View File

@@ -0,0 +1,18 @@
declare module "react-measure" {
const content: any;
export default content;
export type ContentRect = any;
}
declare module "react-plotly.js" {
const content: any;
export default content;
}
declare module "react-svg-pan-zoom" {
export class Tool { }
export type ReactSVGPanZoom = any;
export const ReactSVGPanZoom = undefined as any;
export class Value { }
}

View File

@@ -1,29 +1,30 @@
import * as React from "react";
import { observable, runInAction } from "mobx";
import { observer } from "mobx-react";
import { Loadable } from "./Loadable";
export function makeLazyLoadable<TProps>(
selector: () => Promise<React.ComponentClass<TProps>>
): React.ComponentClass<TProps> {
): React.ComponentClass<TProps> & { preload(): Promise<void> } {
const loader = new Loadable(selector);
@observer
class MyComponent extends React.Component<TProps> {
@observable
component: React.ComponentClass<TProps> | undefined;
async componentDidMount() {
const result = await selector();
runInAction("Set Lazily Loaded Component", () => {
this.component = result;
});
constructor(props: any) {
super(props);
loader.load();
}
render() {
if (!this.component) {
if (!loader.result) {
return <div>Loading...</div>;
}
const C = this.component;
const C = loader.result;
return <C {...this.props} />;
}
static async preload(): Promise<void> {
await loader.load();
}
}
return MyComponent;

View File

@@ -0,0 +1,22 @@
import { observable, runInAction } from "mobx";
export class Loadable<T> {
@observable.ref
public result: T | undefined;
private operation?: Promise<void> = undefined;
constructor(private readonly _load: () => Promise<T>) {}
public load(): Promise<void> {
if (!this.operation) {
this.operation = (async () => {
const r = await this._load();
runInAction(() => {
this.result = r;
});
})();
}
return this.operation;
}
}

View File

@@ -0,0 +1,6 @@
.theme-dark {
--visualizer-background: #263238;
}
.theme-light {
--visualizer-background: white;
}

View File

@@ -2,7 +2,7 @@ import * as React from "react";
import { observer, disposeOnUnmount } from "mobx-react";
import { observable, autorun, trace, computed } from "mobx";
import * as monacoTypes from "monaco-editor";
import { getLanguageId } from "../monaco-text-visualizer/MonacoEditor";
import { getLanguageId } from "../monaco-text-visualizer/getLanguageId";
import {
TreeWithPathView,
TreeViewModel,
@@ -37,7 +37,13 @@ export class AstTree extends React.Component<{
}
return (
<Measure client>
{({ measureRef, contentRect }) => {
{({
measureRef,
contentRect,
}: {
measureRef: any;
contentRect: any;
}) => {
return (
<div
ref={measureRef}

View File

@@ -14,13 +14,13 @@ import {
import { visualizationNs } from "../../consts";
import {
createVisualizer,
ReactVisualization,
globalVisualizationFactory,
createReactVisualization,
} from "@hediet/visualization-core";
import { createTreeViewModelFromTreeNodeData } from "../tree-visualizer";
import { AstTree, NodeInfo } from "./AstVisualizer";
import React from "react";
import LineColumn from "line-column";
import * as React from "react";
import LineColumn = require("line-column");
import * as monacoTypes from "monaco-editor";
import { getLoadedMonaco } from "@hediet/monaco-editor-react";
@@ -54,7 +54,7 @@ export const sAstTreeNode: Serializer<AstTreeNode> = sLazy(() =>
{ inclusive: true }
)
),
}).defineAs(visualizationNs("TreeNodeItem"))
}).defineAs(visualizationNs("AstTreeNodeItem"))
),
segment: sOptionalProp(sString()),
isMarked: sOptionalProp(sBoolean()),
@@ -62,25 +62,26 @@ export const sAstTreeNode: Serializer<AstTreeNode> = sLazy(() =>
start: sNumber(),
length: sNumber(),
}),
}).defineAs(visualizationNs("TreeNode"))
}).defineAs(visualizationNs("AstTreeNode"))
);
export const sAstTree = sOpenObject({
kind: sOpenObject({
ast: sLiteral(true),
tree: sLiteral(true),
text: sLiteral(true),
}),
root: sAstTreeNode,
text: sString(),
fileName: sOptionalProp(sString()),
});
}).defineAs(visualizationNs("AstTreeVisualizationData"));
export const astVisualizer = createVisualizer({
id: "ast",
name: "AST",
serializer: sAstTree,
getVisualization: (data, self) =>
new ReactVisualization(self, { priority: 1500 }, theme => {
createReactVisualization(self, { priority: 1500 }, ({ theme }) => {
const m = createTreeViewModelFromTreeNodeData(
data.root,
node => node.span

View File

@@ -1,3 +1,6 @@
.themeable {
--visualizer-ast-background: var(--visualizer-background);
}
.theme-dark {
--visualizer-ast-marked-background: rgba(234, 92, 0, 0.33);
--visualizer-ast-selected-background: #515c6a;
@@ -8,6 +11,8 @@
}
.component-AstTree {
background: var(--visualizer-ast-background);
.part-editor {
//border-top: solid gray 1px;

View File

@@ -1,7 +1,7 @@
import { observer, disposeOnUnmount } from "mobx-react";
import { observer } from "mobx-react";
import * as React from "react";
import { observable, autorun, runInAction } from "mobx";
import { SvgViewer } from "../../svg-visualizer/SvgViewer";
import { Loadable } from "../../../utils/Loadable";
class VisLoader {
private result: any | undefined;
@@ -24,26 +24,29 @@ class VisLoader {
const vizLoader = new VisLoader();
export function getSvgFromDotCode(dotCode: string): Loadable<string> {
return new Loadable(async () => {
const viz = await vizLoader.getViz();
return await viz.renderString(dotCode);
});
}
@observer
export class GraphvizDotViewer extends React.Component<{
dotCode: string;
svgSource: Loadable<string>;
svgRef?: (element: SVGSVGElement | null) => void;
}> {
@observable private svg: string | null = null;
@disposeOnUnmount
// @ts-ignore
private readonly _updateSvgAutorun = autorun(async () => {
const dotCode = this.props.dotCode; // to trigger mobx dependency
const viz = await vizLoader.getViz();
const svg = await viz.renderString(dotCode);
runInAction("Update svg", () => (this.svg = svg));
});
render() {
if (!this.svg) {
const { svgSource } = this.props;
svgSource.load();
if (!svgSource.result) {
return <div>Loading...</div>;
}
return <SvgViewer svgRef={this.props.svgRef} svgContent={this.svg} />;
return (
<SvgViewer
svgRef={this.props.svgRef}
svgContent={svgSource.result}
/>
);
}
}

View File

@@ -1,11 +1,12 @@
import React from "react";
import * as React from "react";
import { sOpenObject, sLiteral, sString } from "@hediet/semantic-json";
import { GraphvizDotViewer } from "./GraphvizDotVisualizer";
import { getSvgFromDotCode, GraphvizDotViewer } from "./GraphvizDotVisualizer";
import {
ReactVisualization,
globalVisualizationFactory,
createVisualizer,
createReactVisualization,
} from "@hediet/visualization-core";
import { visualizationNs } from "../../../consts";
export const graphvizDotVisualizer = createVisualizer({
id: "graphviz-dot",
@@ -15,11 +16,18 @@ export const graphvizDotVisualizer = createVisualizer({
dotGraph: sLiteral(true),
}),
text: sString(),
}),
getVisualization: (data, self) =>
new ReactVisualization(self, { priority: 1500 }, () => (
<GraphvizDotViewer dotCode={data.text} />
)),
}).defineAs(visualizationNs("GraphvizDotVisualizationData")),
getVisualization: (data, self) => {
const svgSource = getSvgFromDotCode(data.text);
return createReactVisualization(
self,
{
priority: 1500,
preload: () => svgSource.load(),
},
() => <GraphvizDotViewer svgSource={svgSource} />
);
},
});
globalVisualizationFactory.addVisualizer(graphvizDotVisualizer);

View File

@@ -1,36 +0,0 @@
import { observer } from "mobx-react";
import * as React from "react";
import { GraphvizDotViewer } from "../dot-graphviz-visualizer/GraphvizDotVisualizer";
@observer
export class GraphvizGraphViewer extends React.Component<{
nodes: { id: string; label?: string }[];
edges: { from: string; to: string; label?: string }[];
}> {
render() {
const { nodes, edges } = this.props;
const dotContent = `
digraph MyGraph {
${nodes
.map(
n =>
`"${n.id}" [ label = ${JSON.stringify(
n.label !== undefined ? n.label : n.id
)} ];`
)
.join("\n ")}
${edges
.map(
e =>
`"${e.from}" -> "${
e.to
}" [ label = ${JSON.stringify(
e.label !== undefined ? e.label : ""
)} ];`
)
.join("\n")}
}
`;
return <GraphvizDotViewer dotCode={dotContent} />;
}
}

View File

@@ -0,0 +1,34 @@
import { Loadable } from "../../../utils/Loadable";
import { getSvgFromDotCode } from "../dot-graphviz-visualizer/GraphvizDotVisualizer";
export function getSvgFromGraphData(
nodes: { id: string; label?: string }[],
edges: {
from: string;
to: string;
label?: string;
style?: string;
}[]
): Loadable<string> {
const dotContent = `
digraph MyGraph {
${nodes
.map(
n =>
`"${n.id}" [ label = ${JSON.stringify(
n.label !== undefined ? n.label : n.id
)} ];`
)
.join("\n ")}
${edges
.map(
e =>
`"${e.from}" -> "${e.to}" [ label = ${JSON.stringify(
e.label !== undefined ? e.label : ""
)} style = ${JSON.stringify(e.style || "")} ];`
)
.join("\n")}
}
`;
return getSvgFromDotCode(dotContent);
}

View File

@@ -1,20 +1,25 @@
import React from "react";
import { GraphvizGraphViewer } from "./GraphvizGraphVisualizer";
import * as React from "react";
import { sGraph } from "../sGraph";
import {
createVisualizer,
ReactVisualization,
globalVisualizationFactory,
createReactVisualization,
} from "@hediet/visualization-core";
import { GraphvizDotViewer } from "../dot-graphviz-visualizer/GraphvizDotVisualizer";
import { getSvgFromGraphData } from "./getGraphVizData";
export const graphvizGraphVisualizer = createVisualizer({
id: "graphviz-graph",
name: "Graphviz",
serializer: sGraph,
getVisualization: (data, self) =>
new ReactVisualization(self, { priority: 1000 }, () => (
<GraphvizGraphViewer edges={data.edges} nodes={data.nodes} />
)),
getVisualization: (data, self) => {
const svgSource = getSvgFromGraphData(data.nodes, data.edges);
return createReactVisualization(
self,
{ priority: 1000, preload: () => svgSource.load() },
() => <GraphvizDotViewer svgSource={svgSource} />
);
},
});
globalVisualizationFactory.addVisualizer(graphvizGraphVisualizer);

View File

@@ -1,7 +1,7 @@
import { Network, Options, DataSet } from "vis-network/standalone";
import { observer } from "mobx-react";
import * as React from "react";
import { NodeGraphData, EdgeGraphData } from "../sGraph";
import { DataSet, Network, Options } from "vis-network/standalone";
import { EdgeGraphData, NodeGraphData } from "../sGraph";
@observer
export class VisJsGraphViewer extends React.Component<{
@@ -12,7 +12,7 @@ export class VisJsGraphViewer extends React.Component<{
private readonly nodes = new DataSet<{
id: string;
label?: string;
color?: string;
color?: { border: string; background: string } | string;
shape?: string;
}>();
private readonly edges = new DataSet<{
@@ -21,12 +21,18 @@ export class VisJsGraphViewer extends React.Component<{
to: string;
label?: string;
color?: string;
dashes?: boolean;
dashes?: boolean | number[];
shape?: boolean;
}>();
render() {
return <div style={{ height: "100%" }} ref={this.divRef} />;
return (
<div
className="component-VisJsGraphViewer"
style={{ height: "100%" }}
ref={this.divRef}
/>
);
}
synchronizeData() {
@@ -36,11 +42,11 @@ export class VisJsGraphViewer extends React.Component<{
this.nodes.update({
id: n.id,
label: n.label !== undefined ? n.label : n.id,
color: n.color,
color: { background: n.color, border: n.borderColor },
shape: n.shape,
});
}
this.nodes.forEach(item => {
this.nodes.forEach((item) => {
if (!newNodes.has(item.id)) {
this.nodes.remove(item);
}
@@ -63,10 +69,12 @@ export class VisJsGraphViewer extends React.Component<{
from: n.from,
to: n.to,
color: n.color,
dashes: n.dashes,
dashes: { dashed: true, dotted: [1, 4], solid: false }[
n.style || "solid"
],
});
}
this.edges.forEach(item => {
this.edges.forEach((item) => {
if (!newEdges.has(item.id)) {
this.edges.remove(item);
}
@@ -91,6 +99,92 @@ export class VisJsGraphViewer extends React.Component<{
},
},
};
const network = new Network(this.divRef.current!, data, options);
this.network = new Network(this.divRef.current!, data, options);
this.divRef.current!.setAttribute("tabindex", "0");
document.addEventListener("copy", this.onCopy);
}
private network: Network | undefined;
private readonly onCopy = (e: ClipboardEvent) => {
if (!this.network) {
return;
}
if (
!(
document.activeElement &&
document.activeElement.className === "vis-network"
)
) {
return;
}
const n = this.network as any;
const nodesText: string[] = [];
let id = 10;
const visJsIdToNodeId = new Map<string, number>();
// export to draw.io
for (const node of Object.values(n.body.nodes) as any) {
const label = node.shape.options.label;
if (label === undefined) {
continue;
}
let style = "";
if (node.shape.constructor.name === "Ellipse") {
style += "ellipse;";
} else {
style += "rounded=1;";
}
const nodeId = id++;
visJsIdToNodeId.set(node.id, nodeId);
nodesText.push(
`
<mxCell id="${nodeId}" value="${label}" style="${style}" vertex="1" parent="1">
<mxGeometry x="${node.x}" y="${node.y}" width="${node.shape.width}" height="${node.shape.height}" as="geometry"/>
</mxCell>`
);
}
for (const edge of Object.values(n.body.edges) as any) {
const label = edge.options.label || "";
const edgeId = id++;
nodesText.push(
`
<mxCell id="${edgeId}" value="${label}" edge="1" source="${visJsIdToNodeId.get(
edge.from.id
)}" target="${visJsIdToNodeId.get(edge.to.id)}" parent="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>`
);
}
const data = `
<mxGraphModel>
<root>
<mxCell id="0"/>
<mxCell id="1" parent="0"/>
${nodesText.join("\n")}
</root>
</mxGraphModel>`;
/*
<mxCell id="2" value="xxx" edge="1" source="3" target="4" parent="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
*/
e.clipboardData!.setData("text/plain", encodeURIComponent(data));
e.preventDefault();
};
componentWillUnmount() {
document.removeEventListener("copy", this.onCopy);
}
}

View File

@@ -1,8 +1,8 @@
import * as React from "react";
import { makeLazyLoadable } from "../../../utils/LazyLoadable";
import {
createReactVisualization,
createVisualizer,
ReactVisualization,
globalVisualizationFactory,
} from "@hediet/visualization-core";
import { sGraph } from "../sGraph";
@@ -16,12 +16,19 @@ export const visJsGraphVisualizer = createVisualizer({
name: "vis.js",
serializer: sGraph,
getVisualization: (data, self) =>
new ReactVisualization(self, { priority: 1500 }, () => (
<VisJsGraphViewerLazyLoadable
edges={data.edges}
nodes={data.nodes}
/>
)),
createReactVisualization(
self,
{
priority: 1500,
preload: VisJsGraphViewerLazyLoadable.preload,
},
() => (
<VisJsGraphViewerLazyLoadable
edges={data.edges}
nodes={data.nodes}
/>
)
),
});
globalVisualizationFactory.addVisualizer(visJsGraphVisualizer);

View File

@@ -0,0 +1,7 @@
.themeable {
--visualizer-visjs-background: var(--visualizer-background);
}
.component-VisJsGraphViewer {
background-color: var(--visualizer-visjs-background);
}

View File

@@ -1,11 +1,10 @@
import {
sOpenObject,
sLiteral,
sArrayOf,
sString,
sLiteral,
sOpenObject,
sOptionalProp,
sString,
sUnion,
sBoolean,
} from "@hediet/semantic-json";
import { visualizationNs } from "../../consts";
@@ -16,6 +15,7 @@ export const sGraphNode = sOpenObject({
id: sString(),
label: sOptionalProp(sString(), {}),
color: sOptionalProp(sString(), {}),
borderColor: sOptionalProp(sString(), {}),
shape: sOptionalProp(sUnion([sLiteral("ellipse"), sLiteral("box")]), {}),
}).defineAs(visualizationNs("GraphNode"));
@@ -25,11 +25,14 @@ export const sGraphEdge = sOpenObject({
label: sOptionalProp(sString(), {}),
id: sOptionalProp(sString(), {}),
color: sOptionalProp(sString(), {}),
dashes: sOptionalProp(sBoolean(), {}),
style: sOptionalProp(
sUnion([sLiteral("solid"), sLiteral("dashed"), sLiteral("dotted")]),
{}
),
}).defineAs(visualizationNs("GraphEdge"));
export const sGraph = sOpenObject({
kind: sOpenObject({ graph: sLiteral(true) }),
nodes: sArrayOf(sGraphNode),
edges: sArrayOf(sGraphEdge),
}).defineAs(visualizationNs("Graph"));
}).defineAs(visualizationNs("GraphVisualizationData"));

View File

@@ -1,7 +1,6 @@
import * as React from "react";
import { observer } from "mobx-react";
import { computed, action, observable } from "mobx";
import { Icon } from "@blueprintjs/core";
import { sGrid } from ".";
export class DecoratedGridComponent extends React.Component<{
@@ -72,21 +71,14 @@ export class DecoratedGridComponent extends React.Component<{
r.columns.push({
id: m.id,
kind: "empty",
content: (
<Cell>
<Icon
icon="arrow-up"
color="var(--vscode-editor-foreground)"
/>
{m.label || m.id}
</Cell>
),
content: <Cell>{m.label || m.id}</Cell>,
});
}
}
return (
<div
className="component-GridVisualizer"
style={{
height: "100%",
justifyContent: "center",

View File

@@ -1,28 +1,30 @@
import React from "react";
import * as React from "react";
import {
sOpenObject,
sLiteral,
sString,
sArrayOf,
sObject,
sOptionalProp,
sNumber,
} from "@hediet/semantic-json";
import { DecoratedGridComponent } from "./GridVisualizer";
import {
createVisualizer,
ReactVisualization,
globalVisualizationFactory,
createReactVisualization,
} from "@hediet/visualization-core";
import { visualizationNs } from "../../consts";
export const sGrid = sOpenObject({
kind: sOpenObject({
array: sLiteral(true),
grid: sLiteral(true),
}),
columnLabels: sArrayOf(
sOpenObject({
label: sOptionalProp(sString(), {}),
})
columnLabels: sOptionalProp(
sArrayOf(
sOpenObject({
label: sOptionalProp(sString(), {}),
})
)
),
rows: sArrayOf(
sOpenObject({
@@ -55,14 +57,14 @@ export const sGrid = sOpenObject({
),
{}
),
});
}).defineAs(visualizationNs("GridVisualizationData"));
export const gridVisualizer = createVisualizer({
id: "grid",
name: "Grid",
serializer: sGrid,
getVisualization: (data, self) =>
new ReactVisualization(self, { priority: 1000 }, () => (
createReactVisualization(self, { priority: 1000 }, () => (
<DecoratedGridComponent data={data} />
)),
});

View File

@@ -1,3 +1,17 @@
.themeable {
--visualizer-grid-background: var(--visualizer-background);
}
.theme-dark {
--visualizer-grid-empty-foreground: #f0f0f0;
}
.theme-light {
--visualizer-grid-empty-foreground: #000000;
}
.component-GridVisualizer {
background-color: var(--visualizer-grid-background);
}
.component-Grid {
.part-data {
border: 1px solid lightgray;
@@ -9,6 +23,6 @@
}
.part-empty {
color: var(--vscode-editor-foreground);
color: var(--visualizer-grid-empty-foreground);
}
}

View File

@@ -1,13 +1,14 @@
import { sOpenObject, sLiteral, sString } from "@hediet/semantic-json";
import React from "react";
import { sOpenObject, sLiteral, sString, sProp } from "@hediet/semantic-json";
import * as React from "react";
import {
createVisualizer,
ReactVisualization,
globalVisualizationFactory,
createReactVisualization,
} from "@hediet/visualization-core";
import { SvgViewer } from "../svg-visualizer/SvgViewer";
import { Observer, observer, disposeOnUnmount } from "mobx-react";
import { observable, autorun } from "mobx";
import { visualizationNs } from "../../consts";
export const imageVisualizer = createVisualizer({
id: "image",
@@ -16,10 +17,12 @@ export const imageVisualizer = createVisualizer({
kind: sOpenObject({
imagePng: sLiteral(true),
}),
base64Data: sString(),
}),
base64Data: sProp(sString(), {
description: "The base 64 encoded PNG representation of the image",
}),
}).defineAs(visualizationNs("ImageVisualizationData")),
getVisualization: (data, self) =>
new ReactVisualization(self, { priority: 1000 }, () => {
createReactVisualization(self, { priority: 1000 }, () => {
return <ImageViewer base64Data={data.base64Data} />;
}),
});

View File

@@ -3,6 +3,7 @@ export * from "./graph";
export * from "./grid-visualizer";
export * from "./image-visualizer";
export * from "./monaco-text-visualizer";
export * from "./monaco-text-diff-visualizer";
export * from "./perspective-table-visualizer";
export * from "./plotly-visualizer";
export * from "./simple-text-visualizer";

View File

@@ -0,0 +1,124 @@
import * as React from "react";
import { observer, disposeOnUnmount } from "mobx-react";
import { observable, autorun } from "mobx";
import * as monacoTypes from "monaco-editor";
import { Theme } from "@hediet/visualization-core";
import { getLoadedMonaco } from "@hediet/monaco-editor-react";
export function getLanguageId(fileName: string): string {
const l = getLoadedMonaco().languages.getLanguages();
const result = l.find((l) => {
if (l.filenamePatterns) {
for (const p of l.filenamePatterns) {
if (new RegExp(p).test(fileName)) {
return true;
}
}
}
if (l.extensions) {
for (const p of l.extensions) {
if (fileName.endsWith(p)) {
return true;
}
}
}
return false;
});
if (result) {
return result.id;
}
return "text";
}
@observer
export class MonacoDiffEditor extends React.Component<{
originalText: string;
modifiedText: string;
fileName?: string;
theme: Theme;
}> {
@observable private editor:
| monacoTypes.editor.IStandaloneDiffEditor
| undefined;
componentWillUnmount() {
if (this.editor) {
this.editor.dispose();
}
}
get languageId(): string {
if (!this.props.fileName) {
return "text";
}
return getLanguageId(this.props.fileName);
}
private originalModel: monacoTypes.editor.ITextModel | undefined =
undefined;
private modifiedModel: monacoTypes.editor.ITextModel | undefined =
undefined;
@disposeOnUnmount
private _updateText = autorun(() => {
if (this.editor) {
const originalModel = getLoadedMonaco().editor.createModel(
this.props.originalText,
this.languageId,
undefined
);
const modifiedModel = getLoadedMonaco().editor.createModel(
this.props.modifiedText,
this.languageId,
undefined
);
this.editor.setModel({
original: originalModel,
modified: modifiedModel,
});
if (this.originalModel) {
this.originalModel.dispose();
}
this.originalModel = originalModel;
if (this.modifiedModel) {
this.modifiedModel.dispose();
}
this.modifiedModel = modifiedModel;
}
});
private readonly setEditorDiv = (editorDiv: HTMLDivElement) => {
if (!editorDiv) {
return;
}
this.editor = getLoadedMonaco().editor.createDiffEditor(editorDiv, {
automaticLayout: true,
scrollBeyondLastLine: false,
minimap: { enabled: false },
fixedOverflowWidgets: true,
readOnly: true,
theme: this.props.theme.kind === "dark" ? "vs-dark" : "vs",
renderWhitespace: "all",
ignoreTrimWhitespace: false,
});
if (this.originalModel && this.modifiedModel) {
this.editor.setModel({
original: this.originalModel!,
modified: this.modifiedModel!,
});
}
};
render() {
return (
<div className="component-monaco-editor-diff-visualizer">
<div className="part-editor" ref={this.setEditorDiv} />
</div>
);
}
}

View File

@@ -0,0 +1,52 @@
import {
sOpenObject,
sLiteral,
sString,
sOptionalProp,
sProp,
} from "@hediet/semantic-json";
import * as React from "react";
import {
createReactVisualization,
createVisualizer,
globalVisualizationFactory,
} from "@hediet/visualization-core";
import { visualizationNs } from "../../consts";
import { makeLazyLoadable } from "../../utils/LazyLoadable";
const MonacoDiffEditorLazyLoadable = makeLazyLoadable(
async () => (await import("./MonacoEditor")).MonacoDiffEditor
);
export const monacoTextDiffVisualizer = createVisualizer({
id: "monaco-text-diff",
name: "Text Diff",
serializer: sOpenObject({
kind: sOpenObject({
text: sLiteral(true),
}),
text: sProp(sString(), { description: "The text to show" }),
otherText: sProp(sString(), {
description: "The text to compare against",
}),
fileName: sOptionalProp(sString(), {
description:
"An optional filename that might be used for chosing a syntax highlighter",
}),
}).defineAs(visualizationNs("MonacoTextDiffVisualizationData")),
getVisualization: (data, self) =>
createReactVisualization(
self,
{ priority: 900, preload: MonacoDiffEditorLazyLoadable.preload },
({ theme }) => (
<MonacoDiffEditorLazyLoadable
originalText={data.otherText}
modifiedText={data.text}
fileName={data.fileName}
theme={theme}
/>
)
),
});
globalVisualizationFactory.addVisualizer(monacoTextDiffVisualizer);

View File

@@ -0,0 +1,31 @@
.themeable {
--visualizer-monaco-editor-diff-visualizer-background: var(
--visualizer-background
);
}
.component-monaco-editor-diff-visualizer {
background-color: var(
--visualizer-monaco-editor-diff-visualizer-background
);
width: 100%;
height: 100%;
min-width: 0;
box-sizing: border-box;
display: flex;
padding: 2px;
border: 0.5px solid gray;
.part-editor {
min-width: 0;
min-height: 0;
flex: 1;
.current-line {
border: none !important;
}
.selectionHighlight {
background: transparent !important;
}
}
}

View File

@@ -1,43 +1,26 @@
import React from "react";
import * as React from "react";
import { observer, disposeOnUnmount } from "mobx-react";
import { observable, autorun } from "mobx";
import * as monacoTypes from "monaco-editor";
import { Theme } from "@hediet/visualization-core";
import { getLoadedMonaco } from "@hediet/monaco-editor-react";
import { getLanguageId } from "./getLanguageId";
export function getLanguageId(fileName: string): string {
const l = getLoadedMonaco().languages.getLanguages();
const result = l.find(l => {
if (l.filenamePatterns) {
for (const p of l.filenamePatterns) {
if (new RegExp(p).test(fileName)) {
return true;
}
}
}
if (l.extensions) {
for (const p of l.extensions) {
if (fileName.endsWith(p)) {
return true;
}
}
}
return false;
});
if (result) {
return result.id;
}
return "text";
export interface Decoration {
label?: string;
range: monacoTypes.IRange;
}
@observer
export class MonacoEditor extends React.Component<{
text: string;
languageId: string;
theme: Theme;
fileName?: string;
decorations?: Decoration[];
jsonSchemas?: { schema: unknown }[];
}> {
public static nextId = 0;
@observable private editor:
| monacoTypes.editor.IStandaloneCodeEditor
| undefined;
@@ -48,20 +31,119 @@ export class MonacoEditor extends React.Component<{
}
}
get languageId(): string {
if (!this.props.fileName) {
return "text";
}
return getLanguageId(this.props.fileName);
}
private model: monacoTypes.editor.ITextModel | undefined = undefined;
private prevDecorations: string[] = [];
@disposeOnUnmount
private _updateText = autorun(() => {
if (this.editor) {
const decorations =
new Array<monacoTypes.editor.IModelDeltaDecoration>();
const propDecorations = this.props.decorations || [];
const lines = this.props.text.split("\n");
const mappedLines = lines.map((l, idx) => {
if (idx === lines.length - 1) {
return l;
}
if (
!propDecorations.some(
(d) =>
((d.range.endLineNumber === idx + 2 &&
d.range.endColumn === 1) ||
(d.range.startLineNumber === idx + 1 &&
d.range.startColumn >= l.length + 1)) &&
(d.range.startColumn !== d.range.endColumn ||
d.range.startLineNumber !==
d.range.endLineNumber)
)
) {
return l;
}
decorations.push({
range: {
startLineNumber: idx + 1,
endLineNumber: idx + 1,
startColumn: l.length + 1,
endColumn: l.length + 2,
},
options: {
inlineClassName: "decoration-whitespace",
},
});
return l + "␊"; // ␍␊
});
const text = mappedLines.join("\n");
const modelId = MonacoEditor.nextId++;
const model = getLoadedMonaco().editor.createModel(
this.props.text,
this.props.languageId,
undefined
text,
this.languageId,
getLoadedMonaco().Uri.parse(
`inmemory://inmemory/model${modelId}`
)
);
this.editor.setModel(model);
/* this.editor!.updateOptions({ readOnly: false });
if (this.props.jsonSchemas) {
const existingSchemas =
getLoadedMonaco().languages.json.jsonDefaults
.diagnosticsOptions.schemas || [];
getLoadedMonaco().languages.json.jsonDefaults.setDiagnosticsOptions(
{
validate: true,
schemas: [
...existingSchemas.filter(
(s) =>
!s.uri.startsWith(
"https://example.org/schemas/temp/"
)
),
...this.props.jsonSchemas.map((s, idx) => ({
uri: "https://example.org/schemas/temp/" + idx,
fileMatch: [`model${modelId}`],
schema: s.schema,
})),
],
}
);
}
this.prevDecorations = this.editor.deltaDecorations(
[],
[
...decorations,
...propDecorations.map((d) => {
const r = getLoadedMonaco().Range.lift(d.range);
return {
range: r,
options: {
hoverMessage: d.label
? { value: d.label, isTrusted: false }
: undefined,
className: r.isEmpty()
? "decoration-empty"
: "decoration",
},
};
}),
]
);
/*
this.editor!.updateOptions({ readOnly: false });
setTimeout(() => {
const a = this.editor!.getAction(
@@ -71,6 +153,7 @@ export class MonacoEditor extends React.Component<{
this.editor!.updateOptions({ readOnly: true });
});
}, 200);*/
if (this.model) {
this.model.dispose();
}
@@ -90,6 +173,8 @@ export class MonacoEditor extends React.Component<{
fixedOverflowWidgets: true,
readOnly: true,
theme: this.props.theme.kind === "dark" ? "vs-dark" : "vs",
renderWhitespace: "all",
renderValidationDecorations: "on",
});
};

View File

@@ -0,0 +1,28 @@
import { getLoadedMonaco } from "@hediet/monaco-editor-react";
export function getLanguageId(fileName: string): string {
const l = getLoadedMonaco().languages.getLanguages();
const result = l.find(l => {
if (l.filenamePatterns) {
for (const p of l.filenamePatterns) {
if (new RegExp(p).test(fileName)) {
return true;
}
}
}
if (l.extensions) {
for (const p of l.extensions) {
if (fileName.endsWith(p)) {
return true;
}
}
}
return false;
});
if (result) {
return result.id;
}
return "text";
}

View File

@@ -3,14 +3,44 @@ import {
sLiteral,
sString,
sOptionalProp,
sProp,
sArrayOf,
sNumber,
sUnion,
sAny,
} from "@hediet/semantic-json";
import React from "react";
import { getLanguageId, MonacoEditor } from "./MonacoEditor";
import * as React from "react";
import {
createVisualizer,
ReactVisualization,
globalVisualizationFactory,
createReactVisualization,
} from "@hediet/visualization-core";
import { visualizationNs } from "../../consts";
import { makeLazyLoadable } from "../../utils/LazyLoadable";
import { Decoration } from "./MonacoEditor";
export const MonacoEditorLazyLoadable = makeLazyLoadable(
async () => (await import("./MonacoEditor")).MonacoEditor
);
export const LineColumnPosition = sOpenObject({
line: sProp(sNumber(), { description: "The 0-based line number" }),
column: sProp(sNumber(), { description: "The 0-based column number" }),
}).defineAs(visualizationNs("LineColumnPosition"));
export const LineColumnRange = sOpenObject({
start: sProp(LineColumnPosition, { description: "The start position" }),
end: sProp(LineColumnPosition, { description: "The end position" }),
}).defineAs(visualizationNs("LineColumnRange"));
export const OffsetPosition = sNumber().defineAs(
visualizationNs("OffsetPosition")
);
export const OffsetRange = sOpenObject({
start: sProp(OffsetPosition, { description: "The start position" }),
end: sProp(OffsetPosition, { description: "The end position" }),
}).defineAs(visualizationNs("OffsetRange"));
export const monacoTextVisualizer = createVisualizer({
id: "monaco-text",
@@ -19,22 +49,62 @@ export const monacoTextVisualizer = createVisualizer({
kind: sOpenObject({
text: sLiteral(true),
}),
text: sString(),
text: sProp(sString(), { description: "The text to show" }),
decorations: sOptionalProp(
sArrayOf(
sOpenObject({
range: sUnion([LineColumnRange]),
label: sOptionalProp(sString()),
})
)
),
jsonSchemas: sOptionalProp(
sArrayOf(
sOpenObject({
schema: sProp(sAny(), {
description:
"A json schema object that is applied when the fileName indicates a JSON document.",
}),
})
),
{
description:
"An array of schemas used to validate JSON documents.",
}
),
fileName: sOptionalProp(sString(), {
description:
"An optional filename that might be used for chosing a syntax highlighter",
}),
}),
}).defineAs(visualizationNs("MonacoTextVisualizationData")),
getVisualization: (data, self) =>
new ReactVisualization(self, { priority: 500 }, theme => {
let id = "text";
if (data.fileName) {
id = getLanguageId(data.fileName);
createReactVisualization(
self,
{ priority: 500, preload: MonacoEditorLazyLoadable.preload },
function ({ theme }) {
const decorations = data.decorations
? data.decorations.map<Decoration>((d) => ({
label: d.label,
range: {
startLineNumber: d.range.start.line + 1,
startColumn: d.range.start.column + 1,
endLineNumber: d.range.end.line + 1,
endColumn: d.range.end.column + 1,
},
}))
: [];
return (
<MonacoEditorLazyLoadable
text={data.text}
fileName={data.fileName}
theme={theme}
decorations={decorations}
jsonSchemas={data.jsonSchemas}
/>
);
}
return (
<MonacoEditor text={data.text} languageId={id} theme={theme} />
);
}),
),
});
globalVisualizationFactory.addVisualizer(monacoTextVisualizer);

View File

@@ -1,4 +1,9 @@
.themeable {
--visualizer-monaco-editor-background: var(--visualizer-background);
}
.component-monaco-editor {
background-color: var(--visualizer-monaco-editor-background);
width: 100%;
height: 100%;
min-width: 0;
@@ -20,3 +25,17 @@
}
}
}
.decoration-whitespace {
color: black !important;
opacity: 0.4;
}
.decoration-empty {
margin-left: -1px;
border-left: solid rgba(255, 0, 0, 0.815) 3px;
}
.decoration {
background-color: rgba(250, 36, 36, 0.459);
}

View File

@@ -0,0 +1,62 @@
import * as React from "react";
import { Table } from "@finos/perspective";
import "@finos/perspective-viewer";
import "@finos/perspective-viewer-datagrid";
import "@finos/perspective-viewer-d3fc";
import "@finos/perspective-viewer/themes/all-themes.css";
import { HTMLPerspectiveViewerElement } from "@finos/perspective-viewer";
import { observer } from "mobx-react";
import { computed, observable } from "mobx";
@observer
export class PerspectiveDataViewer extends React.Component<
{ data: object[] } & React.HTMLAttributes<HTMLDivElement>
> {
private readonly nodeRef = React.createRef<HTMLPerspectiveViewerElement>();
@observable key = 0;
private oldSchemaStr: string | undefined;
private async replaceData() {
const elem = this.nodeRef.current!;
const data = this.props.data;
const table = (elem as any).worker.table(data) as Table;
const newSchema = await table.schema();
const newSchemaStr = JSON.stringify(newSchema);
const oldSchemaStr = this.oldSchemaStr;
this.oldSchemaStr = newSchemaStr;
if (oldSchemaStr && oldSchemaStr !== newSchemaStr) {
// perspective js does not like it if new data arrives that has a different schema.
// We check if the schema changed an remount the component if it did.
this.key++;
} else {
await elem.load(data);
}
if (!(elem as any)._show_config) {
elem.toggleConfig();
}
}
componentDidUpdate() {
this.replaceData();
}
componentDidMount() {
this.replaceData();
}
render() {
return (
<perspective-viewer
key={this.key}
class={this.props.className}
style={this.props.style}
ref={this.nodeRef}
/>
);
}
}

View File

@@ -1,76 +1,55 @@
import React from "react";
import { sOpenObject, sLiteral, sArrayOf, sAny } from "@hediet/semantic-json";
import * as React from "react";
import { sOpenObject, sLiteral, sArrayOf, sProp } from "@hediet/semantic-json";
import { toJS } from "mobx";
import "@finos/perspective-viewer";
import "@finos/perspective-viewer-datagrid";
import "@finos/perspective-viewer-d3fc";
import "@finos/perspective-viewer/themes/all-themes.css";
import { HTMLPerspectiveViewerElement } from "@finos/perspective-viewer";
import {
createVisualizer,
ReactVisualization,
globalVisualizationFactory,
createReactVisualization,
} from "@hediet/visualization-core";
import { makeLazyLoadable } from "../../utils/LazyLoadable";
import { visualizationNs } from "../../consts";
export const sTable = sOpenObject({
kind: sOpenObject({
table: sLiteral(true),
}),
rows: sArrayOf(
sAny() // object
),
});
rows: sProp(sArrayOf(sOpenObject({})), {
description:
"An array of objects. The properties of the objects are used as columns.",
}),
}).defineAs(visualizationNs("TableVisualizationData"));
const PerspectiveDataViewerLazyLoadable = makeLazyLoadable(
async () => (await import("./PerspectiveDataViewer")).PerspectiveDataViewer
);
export const perspectiveTableVisualizer = createVisualizer({
id: "perspective-table",
name: "Perspective Table",
serializer: sTable,
getVisualization: (data, self) =>
new ReactVisualization(self, { priority: 1000 }, theme => (
<PerspectiveDataViewer
className={
{
light: "perspective-viewer-material",
dark: "perspective-viewer-material-dark",
}[theme.kind]
}
style={{
width: "100%",
height: "100%",
}}
data={toJS(data.rows)}
/>
)),
createReactVisualization(
self,
{
priority: 1000,
preload: PerspectiveDataViewerLazyLoadable.preload,
},
({ theme }) => (
<PerspectiveDataViewerLazyLoadable
className={
{
light: "perspective-viewer-material",
dark: "perspective-viewer-material-dark",
}[theme.kind]
}
style={{
width: "100%",
height: "100%",
}}
data={toJS(data.rows)}
/>
)
),
});
class PerspectiveDataViewer extends React.Component<
{ data: object[] } & React.HTMLAttributes<HTMLDivElement>
> {
private readonly nodeRef = React.createRef<HTMLPerspectiveViewerElement>();
componentDidUpdate() {
this.nodeRef.current!.load(this.props.data);
}
componentDidMount() {
setTimeout(() => {
// TODO: Why timeout?
// It does not work without it.
this.nodeRef.current!.load(this.props.data);
this.nodeRef.current!.toggleConfig();
});
}
render() {
return (
<perspective-viewer
class={this.props.className}
style={this.props.style}
ref={this.nodeRef}
/>
);
}
}
globalVisualizationFactory.addVisualizer(perspectiveTableVisualizer);

View File

@@ -1,32 +1,163 @@
import { Theme } from "@hediet/visualization-core";
import { observer } from "mobx-react";
import * as React from "react";
import Plot from "react-plotly.js";
// needs to be overwriten since the table layout is not defined in @types/plotly.js
interface PlotlyLayoutAnyTemplate extends Plotly.Layout {
template: any;
}
@observer
export class PlotlyViewer extends React.Component<{
data: Plotly.Data[];
layout?: Partial<Plotly.Layout>;
theme: "light" | "dark";
theme: Theme;
onReady: () => void;
}> {
render() {
const { theme, data, layout } = this.props;
const computedLayout = Object.assign(
{},
theme.id === "dark" ? getLayout(theme) : {},
layout || {}
);
if (
data.length === 1 &&
data[0].type === "mesh3d" &&
(layout || {}).margin === undefined
) {
// Fixes https://github.com/JetBrains/rider-debug-visualizer-web-view/issues/6
Object.assign(computedLayout, {
margin: {
l: 0,
r: 0,
b: 0,
t: 0,
pad: 0,
},
});
}
return (
<Plot
style={{ width: "100%", height: "100%" }}
data={data}
layout={Object.assign(
{},
theme === "dark" ? darkLayout : {},
layout || {}
)}
layout={computedLayout}
config={{ responsive: true }}
onInitialized={this.props.onReady}
/>
);
}
}
function getLayout(theme: Theme): Partial<PlotlyLayoutAnyTemplate> {
return {
colorway: [
"#636efa",
"#EF553B",
"#00cc96",
"#ab63fa",
"#19d3f3",
"#e763fa",
"#fecb52",
"#ffa15a",
"#ff6692",
"#b6e880",
],
autosize: true,
font: { color: "#f2f5fa" },
title: { x: 0.05 },
paper_bgcolor: theme.resolveVarToColor("--visualizer-background"), // "rgb(17,17,17)",
plot_bgcolor: theme.resolveVarToColor("--visualizer-background"), // "rgb(17,17,17)",
hovermode: "closest",
polar: {
bgcolor: theme.resolveVarToColor("--visualizer-background"),
angularaxis: {
gridcolor: "#506784",
linecolor: "#506784",
ticks: "",
},
radialaxis: {
gridcolor: "#506784",
linecolor: "#506784",
ticks: "",
},
},
ternary: {
bgcolor: theme.resolveVarToColor("--visualizer-background"),
aaxis: { gridcolor: "#506784", linecolor: "#506784", ticks: "" },
baxis: { gridcolor: "#506784", linecolor: "#506784", ticks: "" },
caxis: { gridcolor: "#506784", linecolor: "#506784", ticks: "" },
},
xaxis: {
gridcolor: "#283442",
linecolor: "#506784",
ticks: "",
zerolinecolor: "#283442",
zerolinewidth: 2,
automargin: true,
},
yaxis: {
gridcolor: "#283442",
linecolor: "#506784",
ticks: "",
zerolinecolor: "#283442",
zerolinewidth: 2,
automargin: true,
},
scene: {
xaxis: {
gridcolor: "#506784",
linecolor: "#506784",
showbackground: false,
ticks: "",
zerolinecolor: "#C8D4E3",
gridwidth: 2,
},
yaxis: {
gridcolor: "#506784",
linecolor: "#506784",
showbackground: false,
ticks: "",
zerolinecolor: "#C8D4E3",
gridwidth: 2,
},
zaxis: {
gridcolor: "#506784",
linecolor: "#506784",
showbackground: false,
ticks: "",
zerolinecolor: "#C8D4E3",
gridwidth: 2,
},
},
geo: {
bgcolor: theme.resolveVarToColor("--visualizer-plotly-background"),
landcolor: theme.resolveVarToColor(
"--visualizer-plotly-background"
),
subunitcolor: "#506784",
showland: true,
showlakes: true,
lakecolor: theme.resolveVarToColor(
"--visualizer-plotly-background"
),
},
template: {
data: {
table: [{
cells: { fill: { color: theme.resolveVarToColor("--visualizer-background") } },
header: { fill: { color: "rgb(17, 17, 17)" } },
}]
}
}
};
}
// extracted from the plotly website
const darkLayout: Partial<Plotly.Layout> = {
const darkLayout: Partial<PlotlyLayoutAnyTemplate> = {
colorway: [
"#636efa",
"#EF553B",
@@ -40,53 +171,6 @@ const darkLayout: Partial<Plotly.Layout> = {
"#b6e880",
],
autosize: true,
//margin: { b: 0, t: 0, pad: 0 },
//height: 300,
/*colorscale: {
sequential: [
[0, "#0508b8"],
[0.0893854748603352, "#1910d8"],
[0.1787709497206704, "#3c19f0"],
[0.2681564245810056, "#6b1cfb"],
[0.3575418994413408, "#981cfd"],
[0.44692737430167595, "#bf1cfd"],
[0.5363128491620112, "#dd2bfd"],
[0.6256983240223464, "#f246fe"],
[0.7150837988826816, "#fc67fd"],
[0.8044692737430168, "#fe88fc"],
[0.8938547486033519, "#fea5fd"],
[0.9832402234636871, "#febefe"],
[1, "#fec3fe"],
],
sequentialminus: [
[0, "#0508b8"],
[0.0893854748603352, "#1910d8"],
[0.1787709497206704, "#3c19f0"],
[0.2681564245810056, "#6b1cfb"],
[0.3575418994413408, "#981cfd"],
[0.44692737430167595, "#bf1cfd"],
[0.5363128491620112, "#dd2bfd"],
[0.6256983240223464, "#f246fe"],
[0.7150837988826816, "#fc67fd"],
[0.8044692737430168, "#fe88fc"],
[0.8938547486033519, "#fea5fd"],
[0.9832402234636871, "#febefe"],
[1, "#fec3fe"],
],
diverging: [
[0, "#8e0152"],
[0.1, "#c51b7d"],
[0.2, "#de77ae"],
[0.3, "#f1b6da"],
[0.4, "#fde0ef"],
[0.5, "#f7f7f7"],
[0.6, "#e6f5d0"],
[0.7, "#b8e186"],
[0.8, "#7fbc41"],
[0.9, "#4d9221"],
[1, "#276419"],
],
},*/
font: { color: "#f2f5fa" },
title: { x: 0.05 },
paper_bgcolor: "rgb(17,17,17)",
@@ -156,17 +240,6 @@ const darkLayout: Partial<Plotly.Layout> = {
gridwidth: 2,
},
},
/*
shapedefaults: {
fillcolor: "#f2f5fa",
line: { width: 0 },
opacity: 0.4,
},
annotationdefaults: {
arrowcolor: "#f2f5fa",
arrowhead: 0,
arrowwidth: 1,
},*/
geo: {
bgcolor: "rgb(17,17,17)",
landcolor: "rgb(17,17,17)",
@@ -175,13 +248,4 @@ const darkLayout: Partial<Plotly.Layout> = {
showlakes: true,
lakecolor: "rgb(17,17,17)",
},
/*
updatemenudefaults: { bgcolor: "#506784", borderwidth: 0 },
sliderdefaults: {
bgcolor: "#C8D4E3",
borderwidth: 1,
bordercolor: "rgb(17,17,17)",
tickwidth: 0,
},
*/
};

View File

@@ -1,23 +1,31 @@
import * as React from "react";
import {
sOpenObject,
sLiteral,
sArrayOf,
sLiteral,
sNull,
sNumber,
sOpenObject,
sOptionalProp,
sAny,
sProp,
sString,
sUnion,
} from "@hediet/semantic-json";
import { makeLazyLoadable } from "../../utils/LazyLoadable";
import { Deferred } from "@hediet/std/synchronization";
import {
createLazyReactVisualization,
createVisualizer,
ReactVisualization,
globalVisualizationFactory,
} from "@hediet/visualization-core";
import * as React from "react";
import { visualizationNs } from "../../consts";
import { makeLazyLoadable } from "../../utils/LazyLoadable";
const PlotlyViewerLazyLoadable = makeLazyLoadable(
async () => (await import("./PlotlyViewer")).PlotlyViewer
);
const sDatum = sUnion([sString(), sNumber(), sNull()]);
const sDatumArr = sUnion([sArrayOf(sDatum), sArrayOf(sArrayOf(sDatum))]);
//const x: Plotly.Data;
export const plotlyVisualizer = createVisualizer({
id: "plotly",
name: "plotly",
@@ -25,23 +33,122 @@ export const plotlyVisualizer = createVisualizer({
kind: sOpenObject({
plotly: sLiteral(true),
}),
data: sProp(sArrayOf(sAny()), {
description:
"Expecting Plotly.Data[] (https://github.com/DefinitelyTyped/DefinitelyTyped/blob/795ce172038dbafcb9cba030d637d733a7eea19c/types/plotly.js/index.d.ts#L1036)",
}),
layout: sOptionalProp(sAny(), {
description:
"Expecting Partial<Plotly.Layout> (https://github.com/DefinitelyTyped/DefinitelyTyped/blob/795ce172038dbafcb9cba030d637d733a7eea19c/types/plotly.js/index.d.ts#L329)",
}),
}),
data: sProp(
sArrayOf(
sOpenObject({
text: sOptionalProp(
sUnion([sString(), sArrayOf(sString())])
),
xaxis: sOptionalProp(sString()),
yaxis: sOptionalProp(sString()),
x: sOptionalProp(sDatumArr),
y: sOptionalProp(sDatumArr),
z: sOptionalProp(sDatumArr),
cells: sOptionalProp(
sOpenObject({
values: sArrayOf(sDatumArr),
})
),
header: sOptionalProp(
sOpenObject({
values: sDatumArr,
})
),
domain: sOptionalProp(
sOpenObject({
x: sArrayOf(sNumber()),
y: sArrayOf(sNumber()),
})
),
type: sOptionalProp(
sUnion([
sLiteral("bar"),
sLiteral("box"),
sLiteral("candlestick"),
sLiteral("choropleth"),
sLiteral("contour"),
sLiteral("heatmap"),
sLiteral("histogram"),
sLiteral("indicator"),
sLiteral("mesh3d"),
sLiteral("ohlc"),
sLiteral("parcoords"),
sLiteral("pie"),
sLiteral("pointcloud"),
sLiteral("scatter"),
sLiteral("scatter3d"),
sLiteral("scattergeo"),
sLiteral("scattergl"),
sLiteral("scatterpolar"),
sLiteral("scatterternary"),
sLiteral("sunburst"),
sLiteral("surface"),
sLiteral("treemap"),
sLiteral("waterfall"),
sLiteral("funnel"),
sLiteral("funnelarea"),
sLiteral("scattermapbox"),
sLiteral("table"),
])
),
mode: sOptionalProp(
sUnion([
sLiteral("lines"),
sLiteral("markers"),
sLiteral("text"),
sLiteral("lines+markers"),
sLiteral("text+markers"),
sLiteral("text+lines"),
sLiteral("text+lines+markers"),
sLiteral("none"),
sLiteral("gauge"),
sLiteral("number"),
sLiteral("delta"),
sLiteral("number+delta"),
sLiteral("gauge+number"),
sLiteral("gauge+number+delta"),
sLiteral("gauge+delta"),
])
),
})
),
{
description:
"Expecting Plotly.Data[] (https://github.com/DefinitelyTyped/DefinitelyTyped/blob/795ce172038dbafcb9cba030d637d733a7eea19c/types/plotly.js/index.d.ts#L1036)",
}
),
layout: sOptionalProp(
sOpenObject({
title: sOptionalProp(sString()),
}),
{
description:
"Expecting Partial<Plotly.Layout> (https://github.com/DefinitelyTyped/DefinitelyTyped/blob/795ce172038dbafcb9cba030d637d733a7eea19c/types/plotly.js/index.d.ts#L329)",
}
),
}).defineAs(visualizationNs("PlotlyVisualizationData")),
getVisualization: (data, self) =>
new ReactVisualization(self, { priority: 1000 }, theme => (
<PlotlyViewerLazyLoadable
data={data.data}
layout={data.layout}
theme={theme.kind}
/>
)),
createLazyReactVisualization(
self,
{
priority: 1000,
preload: PlotlyViewerLazyLoadable.preload,
},
({ theme }) => {
const b = new Deferred();
return {
node: (
<PlotlyViewerLazyLoadable
data={data.data}
layout={data.layout}
theme={theme}
onReady={() => b.resolve}
/>
),
ready: b.promise,
};
}
),
});
globalVisualizationFactory.addVisualizer(plotlyVisualizer);

View File

@@ -0,0 +1,3 @@
.themeable {
--visualizer-plotly-background: var(--visualizer-background);
}

View File

@@ -1,33 +1,62 @@
import { sOpenObject, sLiteral, sString } from "@hediet/semantic-json";
import React from "react";
import * as React from "react";
import {
createVisualizer,
ReactVisualization,
globalVisualizationFactory,
createReactVisualization,
} from "@hediet/visualization-core";
import { visualizationNs } from "../../consts";
export const simpleTextVisualizer = createVisualizer({
// The id must be unique
id: "simple-text",
// The name can be some display text
name: "Simple Text",
// Here you can define the schema for the data that this visualizer can handle.
// You must have a kind property of type object whose properties must be `true`.
serializer: sOpenObject({
kind: sOpenObject({
text: sLiteral(true),
}),
text: sString(),
}),
getVisualization: (data, self) =>
new ReactVisualization(self, { priority: 100 }, () => (
<pre
className="visualizer-simple-text"
style={{
margin: 0,
padding: 10,
color: "var(--visualizer-simple-text-color)",
}}
>
{data.text}
</pre>
)),
}).defineAs(
// This name is important for schema and code generation of the interface types.
// Make sure it is unique. It should end with "VisualizationData".
visualizationNs("SimpleTextVisualizationData")
),
getVisualization: (
/* The type of data is specified by the serializer above. */ data,
self
) =>
// If you want to use react, this is the way to go.
// Return your own implementation if you want to render directly to the DOM.
createReactVisualization(
self,
{
// The priority is used for automatically selecting the best visualization
// if multiple visualizations can handle the data.
priority: 100,
},
() => (
<pre
className="visualizer-simple-text"
style={{
margin: 0,
padding: 10,
// Use css vars for theming.
// See ./style.scss.
color: "var(--visualizer-simple-text-color)",
background: "var(--visualizer-simple-text-background)",
height: "100%",
}}
>
{data.text}
</pre>
)
),
});
// This registers the visualizer for automatic discovery.
// Make sure to import this file in "../index.ts" so that it gets loaded when
// someone imports "@hediet/visualization-bundle"!
globalVisualizationFactory.addVisualizer(simpleTextVisualizer);

View File

@@ -1,3 +1,8 @@
// Provide default colors for both the dark and light theme here!
.themeable {
--visualizer-simple-text-background: var(--visualizer-background);
}
.theme-dark {
--visualizer-simple-text-color: white;
}

View File

@@ -1,24 +1,38 @@
import { sAny } from "@hediet/semantic-json";
import React from "react";
import { JsonSchemaGenerator, sOpenObject } from "@hediet/semantic-json";
import * as React from "react";
import {
createReactVisualization,
createVisualizer,
ReactVisualization,
globalVisualizationFactory,
} from "@hediet/visualization-core";
import { MonacoEditor } from "../monaco-text-visualizer/MonacoEditor";
import { visualizationNs } from "../../consts";
import { MonacoEditorLazyLoadable } from "../monaco-text-visualizer";
export const sourceVisualizer = createVisualizer({
id: "source",
name: "JSON Source",
serializer: sAny(),
serializer: sOpenObject({
kind: sOpenObject({}),
}).defineAs(visualizationNs("SourceVisualizationData")),
getVisualization: (data, self) =>
new ReactVisualization(self, { priority: -100 }, theme => (
<MonacoEditor
text={JSON.stringify(data, undefined, 4)}
languageId={"json"}
theme={theme}
/>
)),
createReactVisualization(self, { priority: -100 }, ({ theme }) => {
const s = new JsonSchemaGenerator();
return (
<MonacoEditorLazyLoadable
text={JSON.stringify(data, undefined, 4)}
fileName={"main.json"}
theme={theme}
jsonSchemas={[
{
schema: s.getJsonSchemaWithDefinitions(
globalVisualizationFactory.getSerializer()
),
},
]}
/>
);
}),
});
globalVisualizationFactory.addHiddenVisualizer(sourceVisualizer);

View File

@@ -74,7 +74,13 @@ export class SvgViewer extends React.Component<{
}
}}
>
{({ measureRef, contentRect }) => (
{({
measureRef,
contentRect,
}: {
measureRef: any;
contentRect: any;
}) => (
<div
ref={measureRef}
className="svgViewer"
@@ -85,7 +91,7 @@ export class SvgViewer extends React.Component<{
height={heightOrDefault(contentRect)}
tool={tool}
value={val as any}
onChangeValue={v => (this.value = v)}
onChangeValue={(v: any) => (this.value = v)}
onChangeTool={this.setTool}
ref={this.svgPanZoomRef}
toolbarProps={{

View File

@@ -1,11 +1,12 @@
import React from "react";
import { sOpenObject, sLiteral, sString } from "@hediet/semantic-json";
import * as React from "react";
import { sOpenObject, sLiteral, sString, sProp } from "@hediet/semantic-json";
import { SvgViewer } from "./SvgViewer";
import {
createVisualizer,
ReactVisualization,
globalVisualizationFactory,
createReactVisualization,
} from "@hediet/visualization-core";
import { visualizationNs } from "../../consts";
export const svgVisualizer = createVisualizer({
id: "svg",
@@ -14,10 +15,10 @@ export const svgVisualizer = createVisualizer({
kind: sOpenObject({
svg: sLiteral(true),
}),
text: sString(),
}),
text: sProp(sString(), { description: "The svg content" }),
}).defineAs(visualizationNs("SvgVisualizationData")),
getVisualization: (data, self) =>
new ReactVisualization(self, { priority: 1500 }, () => (
createReactVisualization(self, { priority: 1500 }, () => (
<SvgViewer svgContent={data.text} />
)),
});

View File

@@ -4,7 +4,10 @@ import { Properties } from "csstype";
type Omit<T, K> = Pick<T, Exclude<keyof T, K>>;
export function omit<T, K extends keyof T>(obj: T, keys: K[]): Omit<T, K> {
export function omit<T extends {}, K extends keyof T>(
obj: T,
keys: K[]
): Omit<T, K> {
const newObj: Omit<T, K> = {} as any;
for (const [key, val] of Object.entries(obj)) {
if (!keys.includes(key as K)) {

View File

@@ -1,7 +1,7 @@
import { observer, disposeOnUnmount } from "mobx-react";
import { observable, autorun, computed, action, runInAction } from "mobx";
import * as React from "react";
import * as classNames from "classnames";
import classNames from "classnames";
import { Rectangle, Point } from "./Point";
import { SvgLine } from "./SvgElements";
import { Item } from ".";
@@ -226,33 +226,14 @@ export class TreeWithPathView extends React.Component<{
<span>
{model.selected.path.reduce((acc, v) => {
acc = acc.slice();
function add() {
acc.push(
<span
className="part-path-item"
key={acc.length}
>
{v}
</span>
);
}
if (isValidFunctionName(v)) {
if (acc.length > 0) {
acc.push(
<span key={acc.length}>.</span>
);
}
add();
} else {
acc.push(
<span key={acc.length}>[</span>
);
add();
acc.push(
<span key={acc.length}>]</span>
);
}
acc.push(
<span
className="part-path-item"
key={acc.length}
>
{v}
</span>
);
return acc;
}, new Array<React.ReactElement>())}
</span>

View File

@@ -15,11 +15,12 @@ import {
Serializer,
sOptionalProp,
sUnion,
sProp,
} from "@hediet/semantic-json";
import {
createVisualizer,
ReactVisualization,
globalVisualizationFactory,
createReactVisualization,
} from "@hediet/visualization-core";
import { visualizationNs } from "../../consts";
@@ -37,25 +38,39 @@ export interface Item {
const sTreeNode: Serializer<TreeNode> = sLazy(() =>
sOpenObject({
children: sArrayOf(sTreeNode),
items: sArrayOf(
sOpenObject({
text: sString(),
emphasis: sOptionalProp(
sUnion(
[
sLiteral("style1"),
sLiteral("style2"),
sLiteral("style3"),
sString(),
],
{ inclusive: true }
)
),
}).defineAs(visualizationNs("TreeNodeItem"))
children: sProp(sArrayOf(sTreeNode), {
description: "The children of this tree-node",
}),
items: sProp(
sArrayOf(
sOpenObject({
text: sProp(sString(), { description: "The text to show" }),
emphasis: sOptionalProp(
sUnion(
[
sLiteral("style1"),
sLiteral("style2"),
sLiteral("style3"),
sString(),
],
{ inclusive: true }
),
{
description: "The style of the text",
}
),
}).defineAs(visualizationNs("TreeNodeItem"))
),
{ description: "The parts that make up the text of this item" }
),
segment: sOptionalProp(sString()),
isMarked: sOptionalProp(sBoolean()),
segment: sOptionalProp(sString(), {
description:
"If a node is selected, the concatenation of all segment values from root to the selected node is shown to the user.",
}),
isMarked: sOptionalProp(sBoolean(), {
description:
"Marked nodes are highlighted and scrolled into view on every visualization update.",
}),
}).defineAs(visualizationNs("TreeNode"))
);
@@ -64,14 +79,14 @@ const sTree = sOpenObject({
tree: sLiteral(true),
}),
root: sTreeNode,
});
}).defineAs(visualizationNs("TreeVisualizationData"));
export const treeVisualizer = createVisualizer({
id: "tree",
name: "Tree",
serializer: sTree,
getVisualization: (data, self) =>
new ReactVisualization(self, { priority: 1200 }, () => {
createReactVisualization(self, { priority: 1200 }, () => {
const m = createTreeViewModelFromTreeNodeData(
data.root,
() => undefined
@@ -98,8 +113,10 @@ export function createTreeViewModelFromTreeNodeData<TData, TNodeData>(
recurse(c, viewModel)
);
let segment = node.segment;
if (!segment && node.items.length > 0) {
let segment;
if (node.segment) {
segment = node.segment;
} else if (node.items.length > 0) {
segment = node.items[0].text;
} else {
segment = "";

View File

@@ -1,3 +1,7 @@
.themeable {
--visualizer-tree-background: var(--visualizer-background);
}
.theme-dark {
--visualizer-tree-childOrThisMarked-background: orange;
--visualizer-tree-childOrThisSelected-background: white;
@@ -124,6 +128,8 @@
}
.component-TreeWithPathView {
background-color: var(--visualizer-tree-background);
display: flex;
flex-direction: column;
height: 100%;

View File

@@ -1,6 +1,10 @@
@import "./src/vars.scss";
@import "./src/visualizers/grid-visualizer/style.scss";
@import "./src/visualizers/simple-text-visualizer/style.scss";
@import "./src/visualizers/monaco-text-visualizer/style.scss";
@import "./src/visualizers/monaco-text-diff-visualizer/style.scss";
@import "./src/visualizers/tree-visualizer/style.scss";
@import "./src/visualizers/ast-visualizer/style.scss";
@import "./src/visualizers/svg-visualizer/style.scss";
@import "./src/visualizers/graph/graph-visjs-visualizer/style.scss";
@import "./src/visualizers/plotly-visualizer/style.scss";

View File

@@ -1,7 +1,7 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "es2020",
"module": "CommonJS",
"moduleResolution": "node",
"strict": true,
"outDir": "dist",

View File

@@ -35,7 +35,7 @@ const visualizations = globalVisualizationFactory.getVisualizations(
/* preferred visualization id */ undefined
);
// `visualizations.bestVisualization` is the visualization that is best suited to visualize the data.
// `visualiztaions.allVisualizations` contains all other available visualizations than can visualize the data.
// `visualiztaions.allVisualizations` contains all suitable visualizations.
```
If you use react, you can use the `VisualizationView` component to render a visualization:
@@ -57,3 +57,7 @@ function App() {
);
}
```
## Architecture
![](./architecture.drawio.svg)

View File

@@ -1,7 +1,7 @@
{
"name": "@hediet/visualization-core",
"description": "Provides infrastructure to find registered visualizations for given json data",
"version": "0.1.0",
"version": "0.3.1",
"license": "MIT",
"homepage": "https://github.com/hediet/visualization",
"repository": {
@@ -27,8 +27,9 @@
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"dependencies": {
"@hediet/semantic-json": "^0.3.15",
"@hediet/std": "^0.6.0",
"@hediet/semantic-json": "^0.3.11",
"mobx": "^5.15.4",
"react": "^16.12.0",
"react-dom": "^16.12.0",
"react-measure": "^2.3.0"

View File

@@ -0,0 +1,30 @@
import * as React from "react";
import { createReactVisualization } from "./ReactVisualization";
import { Visualization, Visualizer } from "./Visualizer";
import { Disposable } from "@hediet/std/disposable";
export function createCanvas2DVisualization(
sourceVisualizer: Visualizer,
options: { priority: number },
render: (context: CanvasRenderingContext2D) => Disposable | void
): Visualization {
return createReactVisualization(
sourceVisualizer,
{ priority: options.priority },
() => (
<canvas
ref={(canvas) => {
if (canvas) {
const ctx = canvas.getContext("2d")!;
ctx.clearRect(0, 0, canvas.width, canvas.height);
// We don't want to use try/catch to swallow the error, so we use setTimeout to protect the caller
setTimeout(() => {
render(ctx);
}, 0);
}
}}
/>
)
);
}

View File

@@ -1,56 +1,105 @@
import * as React from "react";
import * as ReactDom from "react-dom";
import { Visualization, Theme } from "./index";
import { Visualizer, VisualizationId, asVisualizationId } from "./Visualizer";
import {
Visualizer,
VisualizationId,
asVisualizationId,
VisualizationRenderOptions,
} from "./Visualizer";
import { Deferred } from "@hediet/std/synchronization";
interface Options {
id?: string;
name?: string;
interface ReactVisualizationArgs {
priority: number;
preload?: (() => Promise<void>) | undefined;
}
export abstract class BaseReactVisualization implements Visualization<unknown> {
class ReactVisualization implements Visualization<unknown> {
public readonly id: VisualizationId;
public readonly name: string;
public readonly priority: number;
private readonly _preload: (() => Promise<void>) | undefined;
constructor(
public readonly sourceVisualizer: Visualizer,
options: Options
options: ReactVisualizationArgs,
private readonly getReactNode: (
options: ReactVisualizationRenderArgs
) => {
node: React.ReactChild;
ready: Promise<void>;
}
) {
this.id = asVisualizationId(
options.id || sourceVisualizer.id.toString()
);
this.name = options.name || sourceVisualizer.name;
this.id = asVisualizationId(sourceVisualizer.id.toString());
this.name = sourceVisualizer.name;
this.priority = options.priority;
this._preload = options.preload;
}
public preload(): Promise<void> {
return this._preload ? this._preload() : Promise.resolve();
}
public render(
target: HTMLDivElement,
theme: Theme,
previous: unknown | undefined
): unknown {
const node = this.getReactNode(theme);
options: VisualizationRenderOptions<unknown>
): { renderState: unknown; ready: Promise<void> } {
const { node, ready } = this.getReactNode({
theme: options.theme,
});
// We could use this to get access to theme vars:
// getComputedStyle(target).getPropertyValue("--background-color")
ReactDom.render(<>{node}</>, target);
return undefined;
}
protected abstract getReactNode(theme: Theme): React.ReactChild;
}
export class ReactVisualization extends BaseReactVisualization {
constructor(
sourceVisualizer: Visualizer,
options: Options,
private readonly renderReactNode: (theme: Theme) => React.ReactChild
) {
super(sourceVisualizer, options);
}
protected getReactNode(theme: Theme): React.ReactChild {
return this.renderReactNode(theme);
return { renderState: undefined, ready };
}
}
export interface ReactVisualizationRenderArgs {
theme: Theme;
}
export function createReactVisualization(
sourceVisualizer: Visualizer,
args: ReactVisualizationArgs,
renderReactNode: (args: ReactVisualizationRenderArgs) => React.ReactChild
) {
return new ReactVisualization(sourceVisualizer, args, options => {
const deferred = new Deferred();
return {
node: (
<ComponentWithOnMount
onDidMount={() => deferred.resolve()}
child={renderReactNode(options)}
/>
),
ready: deferred.promise,
};
});
}
class ComponentWithOnMount extends React.Component<{
child: React.ReactChild;
onDidMount: () => void;
}> {
componentDidMount() {
this.props.onDidMount();
}
render(): React.ReactNode {
return this.props.child;
}
}
export function createLazyReactVisualization(
sourceVisualizer: Visualizer,
args: ReactVisualizationArgs,
renderReactNode: (
args: ReactVisualizationRenderArgs
) => {
node: React.ReactChild;
ready: Promise<void>;
}
) {
return new ReactVisualization(sourceVisualizer, args, renderReactNode);
}

View File

@@ -0,0 +1,26 @@
import * as semanticJson from "@hediet/semantic-json";
import { createCanvas2DVisualization } from "./CanvasVisualization";
import { CreateVisualizerOptions } from "./createVisualizer";
import { Visualization, Visualizer } from "./Visualizer";
export type RegisterVisualizerFn = (
register: <TData>(
visualizationOptions: CreateVisualizerOptions<TData>
) => void,
lib: Lib
) => void;
interface Lib {
semanticJson: typeof semanticJson;
createCanvas2DVisualization: (
visualizer: Visualizer,
options: { priority: number },
render: (context: CanvasRenderingContext2D) => void
) => Visualization;
}
export const libImplementation: Lib = {
semanticJson,
createCanvas2DVisualization,
};

View File

@@ -1,9 +1,36 @@
export class Theme {
public static readonly light = new Theme("light", "light");
public static readonly dark = new Theme("dark", "dark");
private static _light: Theme;
public static get light(): Theme {
if (!this._light) {
this._light = new Theme("light", "light");
}
return this._light;
}
private static _dark: Theme;
public static get dark(): Theme {
if (!this._dark) {
this._dark = new Theme("dark", "dark");
}
return this._dark;
}
private readonly divElement = document.createElement("div");
constructor(
public readonly id: string,
public readonly kind: "light" | "dark"
) {}
) {
this.divElement.classList.add(
"themeable",
`theme-${this.id}`,
"visualization"
);
this.divElement.style.display = "none";
document.body.append(this.divElement);
}
public resolveVarToColor(varName: string): string {
return getComputedStyle(this.divElement).getPropertyValue(varName);
}
}

View File

@@ -5,6 +5,7 @@ import { Theme } from "./Theme";
export class VisualizationView extends React.Component<{
visualization: Visualization;
theme: Theme;
onReady?: () => void;
}> {
private readonly ref = React.createRef<HTMLDivElement>();
private visualizationRenderState: unknown = undefined;
@@ -19,11 +20,18 @@ export class VisualizationView extends React.Component<{
renderVisualization() {
const { visualization, theme } = this.props;
this.visualizationRenderState = visualization.render(
this.ref.current!,
const { renderState, ready } = visualization.render(this.ref.current!, {
theme,
this.visualizationRenderState
);
previousRenderState: this.visualizationRenderState,
});
ready.then(() => {
if (this.props.onReady) {
return this.props.onReady();
}
});
this.visualizationRenderState = renderState;
}
render() {

View File

@@ -0,0 +1,59 @@
import { BaseSerializer } from "@hediet/semantic-json";
import { Theme } from "./Theme";
export interface Visualizer {
readonly id: VisualizerId;
readonly name: string;
/**
* Allows to deserialize JSON into an visualization.
* This object is also used to inspect the schema of that JSON.
*/
readonly serializer: BaseSerializer<Visualization>;
}
export interface VisualizerId extends String {
__brand: "VisualizerId";
}
export function asVisualizerId(id: string): VisualizerId {
return id as unknown as VisualizerId;
}
export interface Visualization<TRenderState = any> {
readonly id: VisualizationId;
readonly name: string;
readonly priority: number;
render(
target: HTMLDivElement,
options: VisualizationRenderOptions<TRenderState>
): {
renderState: TRenderState;
/**
* Is resolved when the visualization stabilized and is ready to be shown.
* Note that the visualization can be shown before this promise resolves.
* This promise is useful for loading animations or integrations.
*/
ready: Promise<void>;
};
/**
* Preloads resources to speed up first rendering.
* The visualization does not need to be rendered for this.
*/
preload(): Promise<void>;
}
export interface VisualizationId extends String {
__brand: "VisualizationId";
}
export function asVisualizationId(id: string): VisualizationId {
return id as unknown as VisualizationId;
}
export interface VisualizationRenderOptions<TRenderState> {
theme: Theme;
previousRenderState: TRenderState | undefined;
}

View File

@@ -1,37 +0,0 @@
import { BaseSerializer } from "@hediet/semantic-json";
import { Theme } from "./Theme";
export interface VisualizerId extends String {
__brand: "VisualizerId";
}
export function asVisualizerId(id: string): VisualizerId {
return (id as unknown) as VisualizerId;
}
export interface Visualizer {
readonly id: VisualizerId;
readonly name: string;
readonly serializer: BaseSerializer<Visualization>;
}
export interface VisualizationId extends String {
__brand: "VisualizationId";
}
export function asVisualizationId(id: string): VisualizationId {
return (id as unknown) as VisualizationId;
}
export interface Visualization<TRenderState = any> {
readonly id: VisualizationId;
readonly name: string;
readonly priority: number;
readonly sourceVisualizer: Visualizer;
render(
target: HTMLDivElement,
theme: Theme,
previousRenderState: TRenderState | undefined
): TRenderState;
}

View File

@@ -1,6 +1,6 @@
import { Visualizer, Visualization, VisualizationId } from "./Visualizer";
import { VisualizationData } from "./VisualizationData";
import { JSONValue, sUnionMany } from "@hediet/semantic-json";
import { JSONValue, Serializer, sUnionMany } from "@hediet/semantic-json";
import { ObservableMap } from "mobx";
export class VisualizationFactory {
@@ -10,13 +10,17 @@ export class VisualizationFactory {
Visualizer
>();
public getSerializer() {
public getSerializer(): Serializer<[Visualization<any>, Visualizer][]> {
return this._getSerializer(this.visualizers);
}
private _getSerializer(map: ObservableMap<string, Visualizer>) {
private _getSerializer(map: ObservableMap<string, Visualizer>): Serializer<[Visualization<any>, Visualizer][]> {
return sUnionMany(
[...map.values()].map(v => v.serializer.asSerializer()),
[...map.values()].map((v) => v.serializer.asSerializer().refine<[Visualization, Visualizer]>({
canSerialize: (v): v is [Visualization, Visualizer] => false,
fromIntermediate: value => [value, v],
toIntermediate: () => { throw new Error("not supported"); }
})),
{ processingStrategy: "all" }
);
}
@@ -25,6 +29,14 @@ export class VisualizationFactory {
this.visualizers.set(visualizer.id.toString(), visualizer);
}
public getRegisteredVisualizers(): Visualizer[] {
return [...this.visualizers.values()];
}
public getRegisteredHiddenVisualizer(): Visualizer[] {
return [...this.hiddenVisualizers.values()];
}
/**
* Hidden visualizers don't appear in `getSerializer` and should only be presented to the user if he explicitly selects it.
* `getVisualizations(...).bestVisualization` is only the result of a hidden visualizer if the user requests a hidden visualization id.
@@ -38,22 +50,22 @@ export class VisualizationFactory {
preferredVisualization: VisualizationId | undefined
): Visualizations {
const u = this.getSerializer();
const result = u.deserialize((data as unknown) as JSONValue);
const result = u.deserialize(data as unknown as JSONValue);
const allVisualizations = result.value || [];
const u2 = this._getSerializer(this.hiddenVisualizers);
const hiddenResult = u2.deserialize((data as unknown) as JSONValue);
const hiddenResult = u2.deserialize(data as unknown as JSONValue);
allVisualizations.push(...(hiddenResult.value || []));
allVisualizations.sort((a, b) => b.priority - a.priority);
allVisualizations.sort(([a], [b]) => b.priority - a.priority);
let bestVisualization: Visualization | undefined = allVisualizations[0];
if (bestVisualization && bestVisualization.priority < 0) {
let bestVisualization: [Visualization, Visualizer] | undefined = allVisualizations[0];
if (bestVisualization && bestVisualization[0].priority < 0) {
bestVisualization = undefined;
}
if (preferredVisualization) {
const preferred = allVisualizations.find(
vis => vis.id === preferredVisualization
([vis]) => vis.id === preferredVisualization
);
if (preferred) {
bestVisualization = preferred;
@@ -61,8 +73,9 @@ export class VisualizationFactory {
}
return {
bestVisualization,
allVisualizations,
bestVisualization: bestVisualization ? bestVisualization[0] : undefined,
bestVisualizationVisualizer: bestVisualization ? bestVisualization[1] : undefined,
allVisualizations: allVisualizations.map(v => v[0]),
visualizationDataErrors: [],
};
}
@@ -70,6 +83,7 @@ export class VisualizationFactory {
export interface Visualizations {
bestVisualization: Visualization | undefined;
bestVisualizationVisualizer: Visualizer | undefined;
allVisualizations: Visualization[];
visualizationDataErrors: VisualizationDataError[];
}

View File

@@ -1,24 +1,30 @@
import { BaseSerializer } from "@hediet/semantic-json";
import { Visualization, Visualizer, asVisualizerId } from "./Visualizer";
export function createVisualizer<TData>(options: {
export interface CreateVisualizerOptions<TData> {
/**
* A unique id for this visualizer.
*/
id: string;
/**
* A human readable name of this visualizer.
* Should be in English language.
*/
name: string;
serializer: BaseSerializer<TData>;
getVisualization: (data: TData, self: Visualizer) => Visualization;
}): Visualizer {
}
export function createVisualizer<TData>(
options: CreateVisualizerOptions<TData>
): Visualizer {
const result: Visualizer = {
id: asVisualizerId(options.id),
name: options.name,
serializer: options.serializer.refine({
fromIntermediate: data => options.getVisualization(data, result),
fromIntermediate: (data) => options.getVisualization(data, result),
canSerialize: (val): val is Visualization => false,
toIntermediate: () => {
throw new Error("not supported");

View File

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

View File

@@ -1,7 +1,7 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "es2020",
"module": "CommonJS",
"moduleResolution": "node",
"strict": true,
"outDir": "dist",

2352
yarn.lock

File diff suppressed because it is too large Load Diff