mirror of
https://github.com/enso-org/enso.git
synced 2025-01-01 23:39:23 +03:00
Histogram visualization (https://github.com/enso-org/ide/pull/811)
Original commit: 51d9d5a749
This commit is contained in:
parent
162f4b63ec
commit
5a6d920ffe
12
gui/.github/workflows/check.yml
vendored
12
gui/.github/workflows/check.yml
vendored
@ -57,14 +57,20 @@ jobs:
|
||||
toolchain: nightly-2019-11-04
|
||||
override: true
|
||||
|
||||
- name: Install Clippy
|
||||
run: rustup component add clippy
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: '14.15.0'
|
||||
|
||||
- name: Install prettier
|
||||
run: npm install --save-dev --save-exact prettier
|
||||
|
||||
- name: Lint JS
|
||||
run: npx prettier --check "src/**/*.js"
|
||||
|
||||
- name: Install Clippy
|
||||
run: rustup component add clippy
|
||||
|
||||
- name: Building Rust Sources
|
||||
run: node ./run lint --skip-version-validation
|
||||
|
||||
|
2
gui/.prettierignore
Normal file
2
gui/.prettierignore
Normal file
@ -0,0 +1,2 @@
|
||||
**/lib/*
|
||||
**/target/*
|
4
gui/.prettierrc.toml
Normal file
4
gui/.prettierrc.toml
Normal file
@ -0,0 +1,4 @@
|
||||
trailingComma = "es5"
|
||||
tabWidth = 4
|
||||
semi = false
|
||||
singleQuote = true
|
@ -97,13 +97,18 @@ need the following setup:
|
||||
<br/>
|
||||
|
||||
## Working with sources
|
||||
Please note that you should not use a code auto-formatter in this codebase.
|
||||
Please note that you should not use `cargo fmt` on this code base.
|
||||
Please read the following documents to learn more about reasons behind this
|
||||
decision and the recommended code style guide. Be sure to carefully read the
|
||||
[Rust style guide 1](./contributing/style-guide.md)
|
||||
and the [Rust style guide 2](https://dev.enso.org/docs/style-guide/rust.html)
|
||||
before contributing to the codebase.
|
||||
|
||||
We do, however, use [`prettier`](https://prettier.io/) for the JavaScript files in our code base.
|
||||
If you have not installed it already you can do so via `npm install prettier`. To use it manually
|
||||
via command line run `prettier --write` to all JavaScript files in the project. Alternatively,
|
||||
there are plugins for many IDEs available to do this for you.
|
||||
|
||||
### Setting up Engine Services
|
||||
IDE requires a service named Project Manager to be running in the background
|
||||
on the local machine. The service, being part of the Enso Engine, can be
|
||||
|
@ -1,24 +1,24 @@
|
||||
let config = {
|
||||
name: "root",
|
||||
version: "2.0.0-alpha.0",
|
||||
name: 'root',
|
||||
version: '2.0.0-alpha.0',
|
||||
private: true,
|
||||
devDependencies: {
|
||||
"lerna": "^3.20.2",
|
||||
"webpack": "^4.42.1",
|
||||
"webpack-cli": "^3.3.11",
|
||||
"webpack-dev-server": "^3.1.5",
|
||||
lerna: '^3.20.2',
|
||||
webpack: '^4.42.1',
|
||||
'webpack-cli': '^3.3.11',
|
||||
'webpack-dev-server': '^3.1.5',
|
||||
},
|
||||
scripts: {
|
||||
// IMPORTANT! Please remember to run "bootstrap" before "build" or "watch" to install
|
||||
// or update the required dependencies.
|
||||
"bootstrap": "npx lerna bootstrap",
|
||||
"build": "npx lerna run build --stream",
|
||||
"clean": "npx lerna clean -y && rm -Rf node_modules",
|
||||
"dist": "npx lerna run build --stream && npx lerna run dist --stream",
|
||||
"install": "npm install lerna && npx lerna bootstrap",
|
||||
"start": "npm run build && npx lerna run start --stream -- -- ",
|
||||
"watch": "npx lerna run watch --stream"
|
||||
}
|
||||
bootstrap: 'npx lerna bootstrap',
|
||||
build: 'npx lerna run build --stream',
|
||||
clean: 'npx lerna clean -y && rm -Rf node_modules',
|
||||
dist: 'npx lerna run build --stream && npx lerna run dist --stream',
|
||||
install: 'npm install lerna && npx lerna bootstrap',
|
||||
start: 'npm run build && npx lerna run start --stream -- -- ',
|
||||
watch: 'npx lerna run watch --stream',
|
||||
},
|
||||
}
|
||||
|
||||
module.exports = {config}
|
||||
module.exports = { config }
|
||||
|
@ -20,14 +20,27 @@ pub fn table_view_visualization() -> visualization::java_script::FallibleDefinit
|
||||
|
||||
/// Return a `JavaScript` Scatter plot visualization.
|
||||
pub fn scatter_plot_visualization() -> visualization::java_script::FallibleDefinition {
|
||||
let source = include_str!("java_script/scatterPlot.js");
|
||||
let loading_scripts = include_str!("java_script/loading.js");
|
||||
let source = include_str!("java_script/scatterPlot.js");
|
||||
let source = format!("{}{}",loading_scripts,source);
|
||||
|
||||
visualization::java_script::Definition::new(data::builtin_library(),source)
|
||||
}
|
||||
|
||||
/// Return a `JavaScript` Histogram visualization.
|
||||
pub fn histogram_visualization() -> visualization::java_script::FallibleDefinition {
|
||||
let loading_scripts = include_str!("java_script/loading.js");
|
||||
let source = include_str!("java_script/histogram.js");
|
||||
let source = format!("{}{}",loading_scripts,source);
|
||||
|
||||
visualization::java_script::Definition::new(data::builtin_library(),source)
|
||||
}
|
||||
|
||||
/// Return a `JavaScript` Map visualization.
|
||||
pub fn map_view_visualization() -> visualization::java_script::FallibleDefinition {
|
||||
let source = include_str!("java_script/geoMap.js");
|
||||
let loading_scripts = include_str!("java_script/loading.js");
|
||||
let source = include_str!("java_script/geoMap.js");
|
||||
let source = format!("{}{}",loading_scripts,source);
|
||||
|
||||
visualization::java_script::Definition::new(data::builtin_library(),source)
|
||||
}
|
||||
|
@ -1,38 +1,42 @@
|
||||
class BubbleVisualization extends Visualization {
|
||||
static inputType = "Any"
|
||||
static inputType = 'Any'
|
||||
|
||||
onDataReceived(data) {
|
||||
const xmlns = "http://www.w3.org/2000/svg";
|
||||
const xmlns = 'http://www.w3.org/2000/svg'
|
||||
while (this.dom.firstChild) {
|
||||
this.dom.removeChild(this.dom.lastChild);
|
||||
this.dom.removeChild(this.dom.lastChild)
|
||||
}
|
||||
const width = this.dom.getAttributeNS(null, "width");
|
||||
const height = this.dom.getAttributeNS(null, "height");
|
||||
const width = this.dom.getAttributeNS(null, 'width')
|
||||
const height = this.dom.getAttributeNS(null, 'height')
|
||||
|
||||
const svgElem = document.createElementNS(xmlns, "svg");
|
||||
svgElem.setAttributeNS(null, "class" , "vis-svg");
|
||||
svgElem.setAttributeNS(null, "viewBox" , 0 + " " + 0 + " " + width + " " + height);
|
||||
svgElem.setAttributeNS(null, "width" , "100%");
|
||||
svgElem.setAttributeNS(null, "height" , "100%");
|
||||
svgElem.setAttributeNS(null, "transform", "matrix(1 0 0 -1 0 0)");
|
||||
const svgElem = document.createElementNS(xmlns, 'svg')
|
||||
svgElem.setAttributeNS(null, 'class', 'vis-svg')
|
||||
svgElem.setAttributeNS(
|
||||
null,
|
||||
'viewBox',
|
||||
0 + ' ' + 0 + ' ' + width + ' ' + height
|
||||
)
|
||||
svgElem.setAttributeNS(null, 'width', '100%')
|
||||
svgElem.setAttributeNS(null, 'height', '100%')
|
||||
svgElem.setAttributeNS(null, 'transform', 'matrix(1 0 0 -1 0 0)')
|
||||
|
||||
this.dom.appendChild(svgElem);
|
||||
this.dom.appendChild(svgElem)
|
||||
|
||||
data.forEach(data => {
|
||||
const bubble = document.createElementNS(xmlns,"circle");
|
||||
bubble.setAttributeNS(null,"stroke", "black");
|
||||
bubble.setAttributeNS(null,"fill" , "red");
|
||||
bubble.setAttributeNS(null,"r" , data[2] || 0);
|
||||
bubble.setAttributeNS(null,"cx" , data[0] || 0);
|
||||
bubble.setAttributeNS(null,"cy" , data[1] || 0);
|
||||
svgElem.appendChild(bubble);
|
||||
});
|
||||
data.forEach((data) => {
|
||||
const bubble = document.createElementNS(xmlns, 'circle')
|
||||
bubble.setAttributeNS(null, 'stroke', 'black')
|
||||
bubble.setAttributeNS(null, 'fill', 'red')
|
||||
bubble.setAttributeNS(null, 'r', data[2] || 0)
|
||||
bubble.setAttributeNS(null, 'cx', data[0] || 0)
|
||||
bubble.setAttributeNS(null, 'cy', data[1] || 0)
|
||||
svgElem.appendChild(bubble)
|
||||
})
|
||||
}
|
||||
|
||||
setSize(size) {
|
||||
this.dom.setAttributeNS(null, "width", size[0]);
|
||||
this.dom.setAttributeNS(null, "height", size[1]);
|
||||
this.dom.setAttributeNS(null, 'width', size[0])
|
||||
this.dom.setAttributeNS(null, 'height', size[1])
|
||||
}
|
||||
}
|
||||
|
||||
return BubbleVisualization;
|
||||
return BubbleVisualization
|
||||
|
@ -9,45 +9,45 @@
|
||||
* All the limits of API are listed here: https://docs.mapbox.com/api/#rate-limits
|
||||
*/
|
||||
const TOKEN =
|
||||
"pk.eyJ1IjoiZW5zby1vcmciLCJhIjoiY2tmNnh5MXh2MGlyOTJ5cWdubnFxbXo4ZSJ9.3KdAcCiiXJcSM18nwk09-Q";
|
||||
const GEO_POINT = "Geo_Point";
|
||||
const GEO_MAP = "Geo_Map";
|
||||
const SCATTERPLOT_LAYER = "Scatterplot_Layer";
|
||||
const DEFAULT_POINT_RADIUS = 150;
|
||||
'pk.eyJ1IjoiZW5zby1vcmciLCJhIjoiY2tmNnh5MXh2MGlyOTJ5cWdubnFxbXo4ZSJ9.3KdAcCiiXJcSM18nwk09-Q'
|
||||
const GEO_POINT = 'Geo_Point'
|
||||
const GEO_MAP = 'Geo_Map'
|
||||
const SCATTERPLOT_LAYER = 'Scatterplot_Layer'
|
||||
const DEFAULT_POINT_RADIUS = 150
|
||||
|
||||
const DEFAULT_MAP_ZOOM = 11;
|
||||
const DARK_ACCENT_COLOR = [222, 162, 47];
|
||||
const LIGHT_ACCENT_COLOR = [1, 234, 146];
|
||||
const DEFAULT_MAP_ZOOM = 11
|
||||
const DARK_ACCENT_COLOR = [222, 162, 47]
|
||||
const LIGHT_ACCENT_COLOR = [1, 234, 146]
|
||||
|
||||
// =====================================
|
||||
// === Script & Style Initialisation ===
|
||||
// =====================================
|
||||
|
||||
function loadScript(url) {
|
||||
var script = document.createElement("script");
|
||||
script.src = url;
|
||||
var script = document.createElement('script')
|
||||
script.src = url
|
||||
|
||||
document.head.appendChild(script);
|
||||
document.head.appendChild(script)
|
||||
}
|
||||
|
||||
function loadStyle(url) {
|
||||
var link = document.createElement("link");
|
||||
link.href = url;
|
||||
link.rel = "stylesheet";
|
||||
var link = document.createElement('link')
|
||||
link.href = url
|
||||
link.rel = 'stylesheet'
|
||||
|
||||
document.head.appendChild(link);
|
||||
document.head.appendChild(link)
|
||||
}
|
||||
|
||||
loadScript("https://unpkg.com/deck.gl@latest/dist.min.js");
|
||||
loadScript("https://api.tiles.mapbox.com/mapbox-gl-js/v1.6.1/mapbox-gl.js");
|
||||
loadStyle("https://api.tiles.mapbox.com/mapbox-gl-js/v1.6.1/mapbox-gl.css");
|
||||
loadScript('https://unpkg.com/deck.gl@latest/dist.min.js')
|
||||
loadScript('https://api.tiles.mapbox.com/mapbox-gl-js/v1.6.1/mapbox-gl.js')
|
||||
loadStyle('https://api.tiles.mapbox.com/mapbox-gl-js/v1.6.1/mapbox-gl.css')
|
||||
|
||||
const styleHead = document.createElement("style");
|
||||
const styleHead = document.createElement('style')
|
||||
styleHead.innerText = `
|
||||
.mapboxgl-map {
|
||||
border-radius: 14px;
|
||||
}`;
|
||||
document.head.appendChild(styleHead);
|
||||
}`
|
||||
document.head.appendChild(styleHead)
|
||||
|
||||
// ====================
|
||||
// === Id Generator ===
|
||||
@ -60,15 +60,15 @@ document.head.appendChild(styleHead);
|
||||
* @returns {function(): string}
|
||||
*/
|
||||
function makeGenerator() {
|
||||
let _id = 0;
|
||||
return () => {
|
||||
const id = "map_" + _id.toString();
|
||||
_id += 1;
|
||||
return id;
|
||||
};
|
||||
let _id = 0
|
||||
return () => {
|
||||
const id = 'map_' + _id.toString()
|
||||
_id += 1
|
||||
return id
|
||||
}
|
||||
}
|
||||
|
||||
const makeId = makeGenerator();
|
||||
const makeId = makeGenerator()
|
||||
|
||||
// ============================
|
||||
// === MapViewVisualization ===
|
||||
@ -97,237 +97,246 @@ const makeId = makeGenerator();
|
||||
* }
|
||||
*/
|
||||
class GeoMapVisualization extends Visualization {
|
||||
static inputType = "Any";
|
||||
static label = "Geo Map";
|
||||
static inputType = 'Any'
|
||||
static label = 'Geo Map'
|
||||
|
||||
constructor(api) {
|
||||
super(api);
|
||||
this.initMapElement();
|
||||
this.initStyle();
|
||||
this.dataPoints = [];
|
||||
}
|
||||
|
||||
initMapElement() {
|
||||
const width = this.dom.getAttributeNS(null, "width");
|
||||
const height = this.dom.getAttributeNS(null, "height");
|
||||
const mapElem = document.createElement("div");
|
||||
this.mapId = makeId();
|
||||
mapElem.setAttributeNS(null, "id", this.mapId);
|
||||
mapElem.setAttributeNS(
|
||||
null,
|
||||
"style",
|
||||
"width:" + width + "px;height: " + height + "px;"
|
||||
);
|
||||
this.dom.appendChild(mapElem);
|
||||
this.mapElem = mapElem;
|
||||
}
|
||||
|
||||
initStyle() {
|
||||
let defaultMapStyle = "mapbox://styles/mapbox/light-v9";
|
||||
let accentColor = LIGHT_ACCENT_COLOR;
|
||||
if (document.getElementById("root").classList.contains("dark-theme")) {
|
||||
defaultMapStyle = "mapbox://styles/mapbox/dark-v9";
|
||||
accentColor = DARK_ACCENT_COLOR;
|
||||
constructor(api) {
|
||||
super(api)
|
||||
this.initMapElement()
|
||||
this.initStyle()
|
||||
this.dataPoints = []
|
||||
}
|
||||
this.defaultMapStyle = defaultMapStyle;
|
||||
this.accentColor = accentColor;
|
||||
}
|
||||
|
||||
onDataReceived(data) {
|
||||
let parsedData = data;
|
||||
if (typeof data === "string") {
|
||||
parsedData = JSON.parse(data);
|
||||
initMapElement() {
|
||||
const width = this.dom.getAttributeNS(null, 'width')
|
||||
const height = this.dom.getAttributeNS(null, 'height')
|
||||
const mapElem = document.createElement('div')
|
||||
this.mapId = makeId()
|
||||
mapElem.setAttributeNS(null, 'id', this.mapId)
|
||||
mapElem.setAttributeNS(
|
||||
null,
|
||||
'style',
|
||||
'width:' + width + 'px;height: ' + height + 'px;'
|
||||
)
|
||||
this.dom.appendChild(mapElem)
|
||||
this.mapElem = mapElem
|
||||
}
|
||||
this.updateState(parsedData);
|
||||
this.updateMap();
|
||||
this.updateLayers();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the internal data with the new incoming data. Does not affect anything rendered.
|
||||
*/
|
||||
updateState(data) {
|
||||
let { latitude, longitude } = this.prepareDataPoints(
|
||||
data,
|
||||
this.dataPoints,
|
||||
this.accentColor
|
||||
);
|
||||
|
||||
this.latitude = ok(data.latitude) ? data.latitude : latitude;
|
||||
this.longitude = ok(data.longitude) ? data.longitude : longitude;
|
||||
|
||||
// TODO : Compute zoom somehow from span of latitudes and longitudes.
|
||||
this.zoom = ok(data.zoom) ? data.zoom : DEFAULT_MAP_ZOOM;
|
||||
this.mapStyle = ok(data.mapStyle) ? data.mapStyle : this.defaultMapStyle;
|
||||
this.pitch = ok(data.pitch) ? data.pitch : 0;
|
||||
this.controller = ok(data.controller) ? data.controller : true;
|
||||
}
|
||||
|
||||
viewState() {
|
||||
return {
|
||||
longitude: this.longitude,
|
||||
latitude: this.latitude,
|
||||
zoom: this.zoom,
|
||||
pitch: this.pitch,
|
||||
};
|
||||
}
|
||||
|
||||
updateMap() {
|
||||
if (!ok(this.deckgl)) {
|
||||
this.initDeckGl();
|
||||
} else {
|
||||
this.updateDeckGl();
|
||||
initStyle() {
|
||||
let defaultMapStyle = 'mapbox://styles/mapbox/light-v9'
|
||||
let accentColor = LIGHT_ACCENT_COLOR
|
||||
if (document.getElementById('root').classList.contains('dark-theme')) {
|
||||
defaultMapStyle = 'mapbox://styles/mapbox/dark-v9'
|
||||
accentColor = DARK_ACCENT_COLOR
|
||||
}
|
||||
this.defaultMapStyle = defaultMapStyle
|
||||
this.accentColor = accentColor
|
||||
}
|
||||
}
|
||||
|
||||
makeScatterLayer() {
|
||||
return new deck.ScatterplotLayer({
|
||||
data: this.dataPoints,
|
||||
getFillColor: (d) => d.color,
|
||||
getRadius: (d) => d.radius,
|
||||
});
|
||||
}
|
||||
onDataReceived(data) {
|
||||
let parsedData = data
|
||||
if (typeof data === 'string') {
|
||||
parsedData = JSON.parse(data)
|
||||
}
|
||||
this.updateState(parsedData)
|
||||
this.updateMap()
|
||||
this.updateLayers()
|
||||
}
|
||||
|
||||
initDeckGl() {
|
||||
this.deckgl = new deck.DeckGL({
|
||||
container: this.mapId,
|
||||
mapboxApiAccessToken: TOKEN,
|
||||
mapStyle: this.mapStyle,
|
||||
initialViewState: this.viewState(),
|
||||
controller: this.controller,
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Update the internal data with the new incoming data. Does not affect anything rendered.
|
||||
*/
|
||||
updateState(data) {
|
||||
let { latitude, longitude } = this.prepareDataPoints(
|
||||
data,
|
||||
this.dataPoints,
|
||||
this.accentColor
|
||||
)
|
||||
|
||||
updateDeckGl() {
|
||||
this.deckgl.mapStyle = this.mapStyle;
|
||||
this.deckgl.controller = this.controller;
|
||||
}
|
||||
this.latitude = ok(data.latitude) ? data.latitude : latitude
|
||||
this.longitude = ok(data.longitude) ? data.longitude : longitude
|
||||
|
||||
updateLayers() {
|
||||
this.deckgl.setProps({
|
||||
layers: [this.makeScatterLayer()],
|
||||
});
|
||||
}
|
||||
// TODO : Compute zoom somehow from span of latitudes and longitudes.
|
||||
this.zoom = ok(data.zoom) ? data.zoom : DEFAULT_MAP_ZOOM
|
||||
this.mapStyle = ok(data.mapStyle) ? data.mapStyle : this.defaultMapStyle
|
||||
this.pitch = ok(data.pitch) ? data.pitch : 0
|
||||
this.controller = ok(data.controller) ? data.controller : true
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares data points to be shown on the map.
|
||||
*
|
||||
* It checks the type of input data, whether user wants to display single `GEO_POINT`, array of
|
||||
* those, `SCATTERPLOT_LAYER` or a fully defined `GEO_MAP`, and prepares data field of deck.gl
|
||||
* layer for given input.
|
||||
*
|
||||
* @param preparedDataPoints - List holding data points to push the GeoPoints into.
|
||||
* @param parsedData - All the parsed data to create points from.
|
||||
* @param accentColor - accent color of IDE if element doesn't specify one.
|
||||
*/
|
||||
prepareDataPoints(parsedData, preparedDataPoints, accentColor) {
|
||||
let latitude = 0.0;
|
||||
let longitude = 0.0;
|
||||
viewState() {
|
||||
return {
|
||||
longitude: this.longitude,
|
||||
latitude: this.latitude,
|
||||
zoom: this.zoom,
|
||||
pitch: this.pitch,
|
||||
}
|
||||
}
|
||||
|
||||
if (parsedData.type === GEO_POINT) {
|
||||
this.pushGeoPoint(preparedDataPoints, parsedData, accentColor);
|
||||
latitude = parsedData.latitude;
|
||||
longitude = parsedData.longitude;
|
||||
} else if (Array.isArray(parsedData) && parsedData.length) {
|
||||
const computed = this.calculateExtremesAndPushPoints(
|
||||
parsedData,
|
||||
updateMap() {
|
||||
if (!ok(this.deckgl)) {
|
||||
this.initDeckGl()
|
||||
} else {
|
||||
this.updateDeckGl()
|
||||
}
|
||||
}
|
||||
|
||||
makeScatterLayer() {
|
||||
return new deck.ScatterplotLayer({
|
||||
data: this.dataPoints,
|
||||
getFillColor: (d) => d.color,
|
||||
getRadius: (d) => d.radius,
|
||||
})
|
||||
}
|
||||
|
||||
initDeckGl() {
|
||||
this.deckgl = new deck.DeckGL({
|
||||
container: this.mapId,
|
||||
mapboxApiAccessToken: TOKEN,
|
||||
mapStyle: this.mapStyle,
|
||||
initialViewState: this.viewState(),
|
||||
controller: this.controller,
|
||||
})
|
||||
}
|
||||
|
||||
updateDeckGl() {
|
||||
this.deckgl.mapStyle = this.mapStyle
|
||||
this.deckgl.controller = this.controller
|
||||
}
|
||||
|
||||
updateLayers() {
|
||||
this.deckgl.setProps({
|
||||
layers: [this.makeScatterLayer()],
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares data points to be shown on the map.
|
||||
*
|
||||
* It checks the type of input data, whether user wants to display single `GEO_POINT`, array of
|
||||
* those, `SCATTERPLOT_LAYER` or a fully defined `GEO_MAP`, and prepares data field of deck.gl
|
||||
* layer for given input.
|
||||
*
|
||||
* @param preparedDataPoints - List holding data points to push the GeoPoints into.
|
||||
* @param parsedData - All the parsed data to create points from.
|
||||
* @param accentColor - accent color of IDE if element doesn't specify one.
|
||||
*/
|
||||
prepareDataPoints(parsedData, preparedDataPoints, accentColor) {
|
||||
let latitude = 0.0
|
||||
let longitude = 0.0
|
||||
|
||||
if (parsedData.type === GEO_POINT) {
|
||||
this.pushGeoPoint(preparedDataPoints, parsedData, accentColor)
|
||||
latitude = parsedData.latitude
|
||||
longitude = parsedData.longitude
|
||||
} else if (Array.isArray(parsedData) && parsedData.length) {
|
||||
const computed = this.calculateExtremesAndPushPoints(
|
||||
parsedData,
|
||||
preparedDataPoints,
|
||||
accentColor
|
||||
)
|
||||
latitude = computed.latitude
|
||||
longitude = computed.longitude
|
||||
} else {
|
||||
if (
|
||||
parsedData.type === SCATTERPLOT_LAYER &&
|
||||
parsedData.data.length
|
||||
) {
|
||||
const computed = this.calculateExtremesAndPushPoints(
|
||||
parsedData.data,
|
||||
preparedDataPoints,
|
||||
accentColor
|
||||
)
|
||||
latitude = computed.latitude
|
||||
longitude = computed.longitude
|
||||
} else if (parsedData.type === GEO_MAP && ok(parsedData.layers)) {
|
||||
parsedData.layers.forEach((layer) => {
|
||||
if (layer.type === SCATTERPLOT_LAYER) {
|
||||
let dataPoints = layer.data || []
|
||||
const computed = this.calculateExtremesAndPushPoints(
|
||||
dataPoints,
|
||||
preparedDataPoints,
|
||||
accentColor
|
||||
)
|
||||
latitude = computed.latitude
|
||||
longitude = computed.longitude
|
||||
} else {
|
||||
console.warn(
|
||||
'Geo_Map: Currently unsupported deck.gl layer.'
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
return { latitude, longitude }
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper for prepareDataPoints, pushes `GEO_POINT`'s to the list, and calculates central point.
|
||||
* @returns {{latitude: number, longitude: number}} - center.
|
||||
*/
|
||||
calculateExtremesAndPushPoints(
|
||||
dataPoints,
|
||||
preparedDataPoints,
|
||||
accentColor
|
||||
);
|
||||
latitude = computed.latitude;
|
||||
longitude = computed.longitude;
|
||||
} else {
|
||||
if (parsedData.type === SCATTERPLOT_LAYER && parsedData.data.length) {
|
||||
const computed = this.calculateExtremesAndPushPoints(
|
||||
parsedData.data,
|
||||
preparedDataPoints,
|
||||
accentColor
|
||||
);
|
||||
latitude = computed.latitude;
|
||||
longitude = computed.longitude;
|
||||
} else if (parsedData.type === GEO_MAP && ok(parsedData.layers)) {
|
||||
parsedData.layers.forEach((layer) => {
|
||||
if (layer.type === SCATTERPLOT_LAYER) {
|
||||
let dataPoints = layer.data || [];
|
||||
const computed = this.calculateExtremesAndPushPoints(
|
||||
dataPoints,
|
||||
preparedDataPoints,
|
||||
accentColor
|
||||
);
|
||||
latitude = computed.latitude;
|
||||
longitude = computed.longitude;
|
||||
} else {
|
||||
console.warn("Geo_Map: Currently unsupported deck.gl layer.");
|
||||
}
|
||||
});
|
||||
}
|
||||
) {
|
||||
let latitudes = []
|
||||
let longitudes = []
|
||||
dataPoints.forEach((e) => {
|
||||
if (e.type === GEO_POINT) {
|
||||
this.pushGeoPoint(preparedDataPoints, e, accentColor)
|
||||
latitudes.push(e.latitude)
|
||||
longitudes.push(e.longitude)
|
||||
}
|
||||
})
|
||||
let latitude = 0.0
|
||||
let longitude = 0.0
|
||||
if (latitudes.length && longitudes.length) {
|
||||
let minLat = Math.min.apply(null, latitudes)
|
||||
let maxLat = Math.max.apply(null, latitudes)
|
||||
latitude = (minLat + maxLat) / 2
|
||||
let minLon = Math.min.apply(null, longitudes)
|
||||
let maxLon = Math.max.apply(null, longitudes)
|
||||
longitude = (minLon + maxLon) / 2
|
||||
}
|
||||
return { latitude, longitude }
|
||||
}
|
||||
return { latitude, longitude };
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper for prepareDataPoints, pushes `GEO_POINT`'s to the list, and calculates central point.
|
||||
* @returns {{latitude: number, longitude: number}} - center.
|
||||
*/
|
||||
calculateExtremesAndPushPoints(dataPoints, preparedDataPoints, accentColor) {
|
||||
let latitudes = [];
|
||||
let longitudes = [];
|
||||
dataPoints.forEach((e) => {
|
||||
if (e.type === GEO_POINT) {
|
||||
this.pushGeoPoint(preparedDataPoints, e, accentColor);
|
||||
latitudes.push(e.latitude);
|
||||
longitudes.push(e.longitude);
|
||||
}
|
||||
});
|
||||
let latitude = 0.0;
|
||||
let longitude = 0.0;
|
||||
if (latitudes.length && longitudes.length) {
|
||||
let minLat = Math.min.apply(null, latitudes);
|
||||
let maxLat = Math.max.apply(null, latitudes);
|
||||
latitude = (minLat + maxLat) / 2;
|
||||
let minLon = Math.min.apply(null, longitudes);
|
||||
let maxLon = Math.max.apply(null, longitudes);
|
||||
longitude = (minLon + maxLon) / 2;
|
||||
/**
|
||||
* Pushes a new deck.gl-compatible point made out of `GEO_POINT`
|
||||
*
|
||||
* @param preparedDataPoints - List holding geoPoints to push the new element into.
|
||||
* @param geoPoint - `GEO_POINT` to create new deck.gl point from.
|
||||
* @param accentColor - accent color of IDE if `GEO_POINT` doesn't specify one.
|
||||
*/
|
||||
pushGeoPoint(preparedDataPoints, geoPoint, accentColor) {
|
||||
let position = [geoPoint.longitude, geoPoint.latitude]
|
||||
let radius = isNaN(geoPoint.radius)
|
||||
? DEFAULT_POINT_RADIUS
|
||||
: geoPoint.radius
|
||||
let color = ok(geoPoint.color) ? geoPoint.color : accentColor
|
||||
preparedDataPoints.push({ position, color, radius })
|
||||
}
|
||||
return { latitude, longitude };
|
||||
}
|
||||
|
||||
/**
|
||||
* Pushes a new deck.gl-compatible point made out of `GEO_POINT`
|
||||
*
|
||||
* @param preparedDataPoints - List holding geoPoints to push the new element into.
|
||||
* @param geoPoint - `GEO_POINT` to create new deck.gl point from.
|
||||
* @param accentColor - accent color of IDE if `GEO_POINT` doesn't specify one.
|
||||
*/
|
||||
pushGeoPoint(preparedDataPoints, geoPoint, accentColor) {
|
||||
let position = [geoPoint.longitude, geoPoint.latitude];
|
||||
let radius = isNaN(geoPoint.radius)
|
||||
? DEFAULT_POINT_RADIUS
|
||||
: geoPoint.radius;
|
||||
let color = ok(geoPoint.color) ? geoPoint.color : accentColor;
|
||||
preparedDataPoints.push({ position, color, radius });
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets size of the visualization.
|
||||
* @param size - new size, list of two numbers containing width and height respectively.
|
||||
*/
|
||||
setSize(size) {
|
||||
this.dom.setAttributeNS(null, "width", size[0]);
|
||||
this.dom.setAttributeNS(null, "height", size[1]);
|
||||
this.mapElem.setAttributeNS(
|
||||
null,
|
||||
"style",
|
||||
"width:" + size[0] + "px;height: " + size[1] + "px;"
|
||||
);
|
||||
}
|
||||
/**
|
||||
* Sets size of the visualization.
|
||||
* @param size - new size, list of two numbers containing width and height respectively.
|
||||
*/
|
||||
setSize(size) {
|
||||
this.dom.setAttributeNS(null, 'width', size[0])
|
||||
this.dom.setAttributeNS(null, 'height', size[1])
|
||||
this.mapElem.setAttributeNS(
|
||||
null,
|
||||
'style',
|
||||
'width:' + size[0] + 'px;height: ' + size[1] + 'px;'
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if `t` has defined type and is not null.
|
||||
*/
|
||||
function ok(t) {
|
||||
return t !== undefined && t !== null;
|
||||
return t !== undefined && t !== null
|
||||
}
|
||||
|
||||
return GeoMapVisualization;
|
||||
return GeoMapVisualization
|
||||
|
@ -0,0 +1,714 @@
|
||||
/** Histogram Visualization. */
|
||||
// TODO refactor this to avoid loading on startup. See issue #985 .
|
||||
loadScript('https://d3js.org/d3.v4.min.js')
|
||||
loadStyle('https://fontlibrary.org/face/dejavu-sans-mono')
|
||||
|
||||
let shortcuts = {
|
||||
zoomIn: (e) => (e.ctrlKey || e.metaKey) && e.key === 'z',
|
||||
showAll: (e) => (e.ctrlKey || e.metaKey) && e.key === 'a',
|
||||
}
|
||||
|
||||
const LABEL_STYLE = 'font-family: DejaVuSansMonoBook; font-size: 10px;'
|
||||
const MARGIN = 25
|
||||
const X_AXIS_LABEL_WIDTH = 10
|
||||
const Y_AXIS_LABEL_WIDTH = 10
|
||||
const ANIMATION_DURATION = 1000
|
||||
const LINEAR_SCALE = 'linear'
|
||||
const LIGHT_PLOT_COLOR = '#00E890'
|
||||
const DARK_PLOT_COLOR = '#E0A63B'
|
||||
const DEFAULT_NUMBER_OF_BINS = 10
|
||||
const BUTTON_HEIGHT = 25
|
||||
|
||||
/**
|
||||
* A d3.js histogram visualization.
|
||||
*
|
||||
*
|
||||
* Data format (json):
|
||||
{
|
||||
"axis" : {
|
||||
"x" : { "label" : "x-axis label", "scale" : "linear" },
|
||||
"y" : { "label" : "y-axis label", "scale" : "logarithmic" },
|
||||
},
|
||||
"focus" { "x" : 1.7, "y" : 2.1, "zoom" : 3.0 },
|
||||
"color" : "rgb(1.0,0.0,0.0)" },
|
||||
"bins" : 10,
|
||||
"data" : [
|
||||
"values" : [0.1, 0.2, 0.1, 0.15, 0.7],
|
||||
]
|
||||
}
|
||||
*/
|
||||
class Histogram extends Visualization {
|
||||
static inputType = 'Any'
|
||||
static label = 'Histogram'
|
||||
|
||||
onDataReceived(data) {
|
||||
const parsedData = JSON.parse(data)
|
||||
const isUpdate = parsedData.update === 'diff'
|
||||
this.updateState(parsedData, isUpdate)
|
||||
|
||||
if (!this.isInitialised()) {
|
||||
this.initCanvas()
|
||||
this.initLabels()
|
||||
this.initHistogram()
|
||||
}
|
||||
this.updateLabels()
|
||||
this.updateHistogram()
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates whether this visualisation has been initialised.
|
||||
*/
|
||||
isInitialised() {
|
||||
ok(this.svg)
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the internal data and plot settings with the ones from the new incoming data.
|
||||
* If no new settings/data have been provided the old ones will be kept.
|
||||
*/
|
||||
updateState(data, isUpdate) {
|
||||
if (isUpdate) {
|
||||
this._axisSpec = ok(data.axis) ? data.axis : this._axisSpec
|
||||
this._focus = ok(data.focus) ? data.focus : this._focus
|
||||
this._dataValues = ok(data.data.values)
|
||||
? data.data.values
|
||||
: this.data
|
||||
this._bins = ok(data.bins) ? data.bins : this._bins
|
||||
} else {
|
||||
this._axisSpec = data.axis
|
||||
this._focus = data.focus
|
||||
this._dataValues = data.data.values
|
||||
this._bins = data.bins
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the focus area of the histogram. If none is set, returns undefined.
|
||||
*/
|
||||
focus() {
|
||||
return this._focus
|
||||
}
|
||||
|
||||
/**
|
||||
* Return specification for the axis. This includes scales (logarithmic/linear) and labels.
|
||||
*/
|
||||
axisSpec() {
|
||||
return (
|
||||
this._axisSpec || {
|
||||
x: { scale: LINEAR_SCALE },
|
||||
y: { scale: LINEAR_SCALE },
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Return vales to plot.
|
||||
*/
|
||||
data() {
|
||||
return this._dataValues || {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the number of bins to use for the histogram.
|
||||
*/
|
||||
binCount() {
|
||||
if (!ok(this._bins)) {
|
||||
return DEFAULT_NUMBER_OF_BINS
|
||||
} else {
|
||||
return Math.max(1, self._bins)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the layout measurements for the plot. This includes the outer dimensions of the
|
||||
* drawing area as well as the inner dimensions of the plotting area and the margins.
|
||||
*/
|
||||
canvasDimensions() {
|
||||
const width = this.dom.getAttributeNS(null, 'width')
|
||||
let height = this.dom.getAttributeNS(null, 'height')
|
||||
height = height - BUTTON_HEIGHT
|
||||
const margin = this.margins()
|
||||
return {
|
||||
inner: {
|
||||
width: width - margin.left - margin.right,
|
||||
height: height - margin.top - margin.bottom,
|
||||
},
|
||||
outer: { width, height },
|
||||
margin,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialise the drawing svg and related properties, e.g., canvas size and margins.
|
||||
*/
|
||||
initCanvas() {
|
||||
while (this.dom.firstChild) {
|
||||
this.dom.removeChild(this.dom.lastChild)
|
||||
}
|
||||
|
||||
this.canvas = this.canvasDimensions()
|
||||
const container = this.createOuterContainerWithStyle(
|
||||
this.canvas.outer.width,
|
||||
this.canvas.outer.height
|
||||
)
|
||||
this.dom.appendChild(container)
|
||||
|
||||
this.svg = d3
|
||||
.select(container)
|
||||
.append('svg')
|
||||
.attr('width', this.canvas.outer.width)
|
||||
.attr('height', this.canvas.outer.height)
|
||||
.append('g')
|
||||
.attr(
|
||||
'transform',
|
||||
'translate(' +
|
||||
this.canvas.margin.left +
|
||||
',' +
|
||||
this.canvas.margin.top +
|
||||
')'
|
||||
)
|
||||
|
||||
this.yAxis = this.svg.append('g').attr('style', LABEL_STYLE)
|
||||
this.xAxis = this.svg.append('g').attr('style', LABEL_STYLE)
|
||||
|
||||
this.plot = this.svg
|
||||
.append('g')
|
||||
.attr('clip-path', 'url(#hist-clip-path)')
|
||||
|
||||
// Create clip path
|
||||
const defs = this.svg.append('defs')
|
||||
defs.append('clipPath')
|
||||
.attr('id', 'hist-clip-path')
|
||||
.append('rect')
|
||||
.attr('width', this.canvas.inner.width)
|
||||
.attr('height', this.canvas.inner.height)
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialise the histogram with the current data and settings.
|
||||
*/
|
||||
initHistogram() {
|
||||
this.updateHistogram()
|
||||
const zoom = this.initPanAndZoom()
|
||||
// TODO [MM]: In task specification buttons were on top of the visualization, but because
|
||||
// the visualization selector obfuscated them, they're now on the bottom.
|
||||
// This should be fixed in (#898).
|
||||
this.createButtonFitAll(zoom)
|
||||
const selectedZoomBtn = this.createButtonScaleToPoints()
|
||||
this.initBrushing(selectedZoomBtn, zoom)
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialise panning and zooming functionality on the visualization.
|
||||
*/
|
||||
initPanAndZoom() {
|
||||
const canvas = this.canvas
|
||||
const zoomClass = 'zoom'
|
||||
const minScale = 0.5
|
||||
const maxScale = 20
|
||||
const extent = [minScale, maxScale]
|
||||
const zoom = d3
|
||||
.zoom()
|
||||
.filter(function () {
|
||||
let right_button = 2
|
||||
let mid_button = 1
|
||||
let scroll_wheel = 0
|
||||
switch (d3.event.type) {
|
||||
case 'mousedown':
|
||||
return (
|
||||
d3.event.button === right_button ||
|
||||
d3.event.button === mid_button
|
||||
)
|
||||
case 'wheel':
|
||||
return d3.event.button === scroll_wheel
|
||||
default:
|
||||
return false
|
||||
}
|
||||
})
|
||||
.scaleExtent(extent)
|
||||
.extent([
|
||||
[0, 0],
|
||||
[canvas.inner.width, canvas.inner.height],
|
||||
])
|
||||
.on(zoomClass, zoomed)
|
||||
|
||||
const zoomElem = this.svg
|
||||
.append('g')
|
||||
.attr('class', zoomClass)
|
||||
.attr('width', canvas.inner.width)
|
||||
.attr('height', canvas.inner.height)
|
||||
.style('fill', 'none')
|
||||
.style('pointer-events', 'all')
|
||||
.call(zoom)
|
||||
|
||||
const self = this
|
||||
/**
|
||||
* Helper function called on pan/scroll.
|
||||
*/
|
||||
function zoomed() {
|
||||
self.scale.zoom = d3.event.transform.k
|
||||
let tmpScale = Object.assign({}, self.scale)
|
||||
tmpScale.x = d3.event.transform.rescaleX(self.scale.x)
|
||||
self.rescale(tmpScale, false)
|
||||
}
|
||||
|
||||
return { zoomElem, zoom }
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialise brushing functionality on the visualization.
|
||||
*
|
||||
* Brush is a tool which enables user to select points, and zoom into selection via
|
||||
* keyboard shortcut or button event.
|
||||
*/
|
||||
initBrushing(selectedZoomBtn, zoom) {
|
||||
let extent
|
||||
const brushClass = 'brush'
|
||||
|
||||
const brush = d3
|
||||
.brushX()
|
||||
.extent([
|
||||
[0, 0],
|
||||
[this.canvas.inner.width, this.canvas.inner.height],
|
||||
])
|
||||
.on('start ' + brushClass, updateChartOnBrush)
|
||||
|
||||
// The brush element must be child of zoom element - this is only way we found to have both
|
||||
// zoom and brush events working at the same time. See https://stackoverflow.com/a/59757276 .
|
||||
const brushElem = zoom.zoomElem
|
||||
.append('g')
|
||||
.attr('class', brushClass)
|
||||
.call(brush)
|
||||
|
||||
const self = this
|
||||
|
||||
/**
|
||||
* Zooms into selected fragment of plot.
|
||||
*
|
||||
* Based on https://www.d3-graph-gallery.com/graph/interactivity_brush.html
|
||||
* Section "Brushing for zooming".
|
||||
*/
|
||||
const zoomIn = () => {
|
||||
const xMin = self.scale.x.invert(extent[0])
|
||||
const xMax = self.scale.x.invert(extent[1])
|
||||
|
||||
self.scale.x.domain([xMin, xMax])
|
||||
const dx = extent[1] - extent[0]
|
||||
self.scale.zoom = self.scale.zoom * (self.canvas.inner.width / dx)
|
||||
|
||||
self.rescale(self.scale, true)
|
||||
}
|
||||
|
||||
const zoomInKeyEvent = (event) => {
|
||||
if (shortcuts.zoomIn(event)) {
|
||||
zoomIn()
|
||||
endBrushing()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates plot when brushing.
|
||||
*/
|
||||
function updateChartOnBrush() {
|
||||
const selectionEvent = d3.event.selection
|
||||
selectedZoomBtn.style.display = 'inline-block'
|
||||
selectedZoomBtn.addEventListener('click', zoomIn, true)
|
||||
document.addEventListener('keydown', zoomInKeyEvent, true)
|
||||
extent = selectionEvent
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes brush, keyboard event and zoom button when end event is captured.
|
||||
*/
|
||||
const endBrushing = () => {
|
||||
brushElem.call(brush.move, null)
|
||||
selectedZoomBtn.style.display = 'none'
|
||||
selectedZoomBtn.removeEventListener('click', zoomIn, true)
|
||||
document.removeEventListener('keydown', zoomInKeyEvent, true)
|
||||
}
|
||||
|
||||
let endEvents = ['click', 'auxclick', 'contextmenu', 'scroll']
|
||||
endEvents.forEach((e) =>
|
||||
document.addEventListener(e, endBrushing, false)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function for rescaling the data points with a new scale.
|
||||
*/
|
||||
rescale(scale, with_animation) {
|
||||
const animation_duration = with_animation ? ANIMATION_DURATION : 0.0
|
||||
this.xAxis
|
||||
.transition()
|
||||
.duration(animation_duration)
|
||||
.call(d3.axisBottom(scale.x).ticks(this.binCount()))
|
||||
this.plot
|
||||
.selectAll('rect')
|
||||
.transition()
|
||||
.duration(animation_duration)
|
||||
.attr(
|
||||
'transform',
|
||||
(d) =>
|
||||
'translate(' +
|
||||
scale.x(d.x0) +
|
||||
',' +
|
||||
scale.y(d.length) +
|
||||
')scale(' +
|
||||
scale.zoom +
|
||||
',1)'
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the d3 histogram with the current data.
|
||||
*
|
||||
* Binds the new data to the plot, creating new bars, removing old ones and
|
||||
* updates the axes accordingly.
|
||||
*/
|
||||
updateHistogram() {
|
||||
const extremesAndDeltas = this.extremesAndDeltas()
|
||||
const dataPoints = this.data()
|
||||
const focus = this.focus()
|
||||
|
||||
let domain_x = [
|
||||
extremesAndDeltas.xMin - extremesAndDeltas.paddingX,
|
||||
extremesAndDeltas.xMax + extremesAndDeltas.paddingX,
|
||||
]
|
||||
|
||||
if (focus !== undefined) {
|
||||
if (focus.x !== undefined && focus.zoom !== undefined) {
|
||||
let padding_x = extremesAndDeltas.dx * (1 / (2 * focus.zoom))
|
||||
domain_x = [focus.x - padding_x, focus.x + padding_x]
|
||||
}
|
||||
}
|
||||
|
||||
const x = d3
|
||||
.scaleLinear()
|
||||
.domain(domain_x)
|
||||
.range([0, this.canvas.inner.width])
|
||||
|
||||
this.xAxis
|
||||
.attr('transform', 'translate(0,' + this.canvas.inner.height + ')')
|
||||
.call(d3.axisBottom(x))
|
||||
|
||||
const histogram = d3
|
||||
.histogram()
|
||||
.value((d) => d)
|
||||
.domain(x.domain())
|
||||
.thresholds(x.ticks(this.binCount()))
|
||||
|
||||
const bins = histogram(dataPoints)
|
||||
|
||||
const y = d3.scaleLinear().range([this.canvas.inner.height, 0])
|
||||
y.domain([0, d3.max(bins, (d) => d.length)])
|
||||
|
||||
const yAxisTicks = y.ticks().filter((tick) => Number.isInteger(tick))
|
||||
|
||||
const yAxis = d3
|
||||
.axisLeft(y)
|
||||
.tickValues(yAxisTicks)
|
||||
.tickFormat(d3.format('d'))
|
||||
|
||||
this.yAxis.call(yAxis)
|
||||
|
||||
let accentColor = LIGHT_PLOT_COLOR
|
||||
|
||||
if (document.getElementById('root').classList.contains('dark')) {
|
||||
accentColor = DARK_PLOT_COLOR
|
||||
}
|
||||
|
||||
const items = this.plot.selectAll('rect').data(bins)
|
||||
|
||||
this.bars = items
|
||||
.enter()
|
||||
.append('rect')
|
||||
.attr('x', 1)
|
||||
.attr(
|
||||
'transform',
|
||||
(d) => 'translate(' + x(d.x0) + ',' + y(d.length) + ')'
|
||||
)
|
||||
.attr('width', (d) => x(d.x1) - x(d.x0))
|
||||
.attr('height', (d) => this.canvas.inner.height - y(d.length))
|
||||
.style('fill', accentColor)
|
||||
|
||||
items.exit().remove()
|
||||
|
||||
this.scale = { x, y, zoom: 1.0 }
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates labels on axes if they are defined.
|
||||
*/
|
||||
initLabels() {
|
||||
this.yAxisLabel = this.svg
|
||||
.append('text')
|
||||
.attr('text-anchor', 'end')
|
||||
.attr('style', LABEL_STYLE)
|
||||
.attr('transform', 'rotate(-90)')
|
||||
this.xAxisLabel = this.svg
|
||||
.append('text')
|
||||
.attr('text-anchor', 'end')
|
||||
.attr('style', LABEL_STYLE)
|
||||
}
|
||||
|
||||
/**
|
||||
* Update labels with current data.
|
||||
*/
|
||||
updateLabels() {
|
||||
const axis = this.axisSpec()
|
||||
const canvas = this.canvas
|
||||
|
||||
const fontStyle = '10px DejaVuSansMonoBook'
|
||||
if (axis.x.label !== undefined) {
|
||||
this.xAxisLabel
|
||||
.attr(
|
||||
'y',
|
||||
canvas.inner.height +
|
||||
canvas.margin.bottom -
|
||||
X_AXIS_LABEL_WIDTH / 2.0
|
||||
)
|
||||
.attr(
|
||||
'x',
|
||||
canvas.inner.width / 2.0 +
|
||||
this.textWidth(axis.x.label, fontStyle) / 2
|
||||
)
|
||||
.text(axis.x.label)
|
||||
}
|
||||
// Note: y axis is rotated by 90 degrees, so x/y is switched.
|
||||
if (axis.y.label !== undefined) {
|
||||
this.yAxisLabel
|
||||
.attr('y', -canvas.margin.left + Y_AXIS_LABEL_WIDTH)
|
||||
.attr(
|
||||
'x',
|
||||
-canvas.inner.height / 2 +
|
||||
this.textWidth(axis.y.label, fontStyle) / 2
|
||||
)
|
||||
.text(axis.y.label)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the text width. Ensures that labels on x axis wont overlap to keeps them readable.
|
||||
*/
|
||||
textWidth(text, font) {
|
||||
const canvas = document.createElement('canvas')
|
||||
const context = canvas.getContext('2d')
|
||||
context.font = font
|
||||
const metrics = context.measureText(' ' + text)
|
||||
return metrics.width
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the extrema of the data and and paddings that ensure data will fit into the
|
||||
* drawing area.
|
||||
*
|
||||
* It traverses through data getting minimal and maximal values, and calculates padding based on
|
||||
* span calculated from above values, multiplied by 10% so that the plot is a little bit smaller
|
||||
* than the container.
|
||||
*/
|
||||
extremesAndDeltas() {
|
||||
const dataPoints = this.data()
|
||||
let xMin = dataPoints[0]
|
||||
let xMax = dataPoints[0]
|
||||
|
||||
dataPoints.forEach((value) => {
|
||||
if (value < xMin) {
|
||||
xMin = value
|
||||
}
|
||||
if (value > xMax) {
|
||||
xMax = value
|
||||
}
|
||||
})
|
||||
|
||||
const dx = xMax - xMin
|
||||
const paddingX = 0.1 * dx
|
||||
|
||||
return { xMin, xMax, paddingX, dx }
|
||||
}
|
||||
|
||||
/**
|
||||
* Return margins for plots drawing area.
|
||||
*/
|
||||
margins() {
|
||||
const axis = this.axisSpec()
|
||||
const noXAxis = axis.x.label === undefined
|
||||
const noYAxis = axis.y.label === undefined
|
||||
|
||||
const top = MARGIN / 2.0
|
||||
const right = MARGIN / 2.0
|
||||
if (noXAxis && noYAxis) {
|
||||
return { top, right, bottom: MARGIN, left: MARGIN }
|
||||
} else if (noYAxis) {
|
||||
return {
|
||||
top,
|
||||
right,
|
||||
bottom: MARGIN + X_AXIS_LABEL_WIDTH,
|
||||
left: MARGIN,
|
||||
}
|
||||
} else if (noXAxis) {
|
||||
return {
|
||||
top,
|
||||
right,
|
||||
bottom: MARGIN,
|
||||
left: MARGIN + Y_AXIS_LABEL_WIDTH,
|
||||
}
|
||||
}
|
||||
return {
|
||||
top,
|
||||
right,
|
||||
bottom: MARGIN + X_AXIS_LABEL_WIDTH,
|
||||
left: MARGIN + Y_AXIS_LABEL_WIDTH,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates HTML div element as container for plot.
|
||||
*/
|
||||
createOuterContainerWithStyle(width, height) {
|
||||
const divElem = document.createElementNS(null, 'div')
|
||||
divElem.setAttributeNS(null, 'class', 'vis-histogram')
|
||||
divElem.setAttributeNS(
|
||||
null,
|
||||
'viewBox',
|
||||
0 + ' ' + 0 + ' ' + width + ' ' + height
|
||||
)
|
||||
divElem.setAttributeNS(null, 'width', '100%')
|
||||
divElem.setAttributeNS(null, 'height', '100%')
|
||||
|
||||
const addStyleToElem = (attr, stl) => {
|
||||
const style = document.createElement('style')
|
||||
style.innerText = attr + '{' + stl + '}'
|
||||
|
||||
divElem.appendChild(style)
|
||||
}
|
||||
|
||||
const darkStrokeColor = `rgba(255,255,255,0.7)`
|
||||
const buttonLightColor = `#333`
|
||||
const darkBtnHoverColor = `rgba(255,255,255,0.5)`
|
||||
const darkSelectionFill = `#efefef`
|
||||
|
||||
addStyleToElem('.selection', 'rx: 4px;stroke: transparent;')
|
||||
addStyleToElem(
|
||||
'button',
|
||||
`
|
||||
margin-left: 5px;
|
||||
margin-bottom: 5px;
|
||||
display: inline-block;
|
||||
padding: 2px 10px;
|
||||
outline: none;
|
||||
background-color: transparent;
|
||||
border: 1px solid ${buttonLightColor};
|
||||
color: ${buttonLightColor};
|
||||
border-radius: 14px;
|
||||
font-size: 10px;
|
||||
font-family: DejaVuSansMonoBook;
|
||||
vertical-align: top;
|
||||
transition: all 0.3s ease;
|
||||
`
|
||||
)
|
||||
addStyleToElem(
|
||||
'button:hover',
|
||||
`
|
||||
background-color: ${buttonLightColor};
|
||||
color: ${darkSelectionFill};
|
||||
`
|
||||
)
|
||||
|
||||
addStyleToElem(
|
||||
'.dark button',
|
||||
`
|
||||
border: 0;
|
||||
background-color: ${darkStrokeColor};
|
||||
`
|
||||
)
|
||||
addStyleToElem(
|
||||
'.dark button:hover',
|
||||
`
|
||||
background-color: ${darkBtnHoverColor};
|
||||
`
|
||||
)
|
||||
addStyleToElem('.dark .selection', `fill: ${darkSelectionFill}`)
|
||||
addStyleToElem('.dark line', `stroke: ${darkStrokeColor};`)
|
||||
addStyleToElem('.dark .domain', `stroke: ${darkStrokeColor};`)
|
||||
addStyleToElem('.dark text', `fill: ${darkStrokeColor};`)
|
||||
|
||||
return divElem
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a button HTML element with default size.
|
||||
*/
|
||||
createButtonElement() {
|
||||
const btn = document.createElement('button')
|
||||
btn.setAttribute('width', '80px')
|
||||
btn.setAttribute('height', '20px')
|
||||
return btn
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a button that when clicked pans and zooms the plot to fit all data points.
|
||||
*/
|
||||
createButtonFitAll(zoom) {
|
||||
const extremesAndDeltas = this.extremesAndDeltas()
|
||||
const btn = this.createButtonElement()
|
||||
|
||||
let text = document.createTextNode('Fit all')
|
||||
btn.appendChild(text)
|
||||
|
||||
const self = this
|
||||
const reset_zoom_and_pan = () => {
|
||||
zoom.zoomElem
|
||||
.transition()
|
||||
.duration(0)
|
||||
.call(zoom.zoom.transform, d3.zoomIdentity)
|
||||
|
||||
let domain_x = [
|
||||
extremesAndDeltas.xMin - extremesAndDeltas.paddingX,
|
||||
extremesAndDeltas.xMax + extremesAndDeltas.paddingX,
|
||||
]
|
||||
|
||||
self.scale.x.domain(domain_x)
|
||||
self.scale.zoom = 1.0
|
||||
self.rescale(self.scale, true)
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (shortcuts.showAll(e)) {
|
||||
reset_zoom_and_pan()
|
||||
}
|
||||
})
|
||||
|
||||
btn.addEventListener('click', reset_zoom_and_pan)
|
||||
this.dom.appendChild(btn)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a button to zoom into brushed fragment of plot.
|
||||
*/
|
||||
createButtonScaleToPoints() {
|
||||
const btn = this.createButtonElement()
|
||||
const text = document.createTextNode('Zoom to selected')
|
||||
btn.appendChild(text)
|
||||
btn.setAttribute('width', '120px')
|
||||
btn.style.display = 'none'
|
||||
this.dom.appendChild(btn)
|
||||
return btn
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets size of the main parent DOM object.
|
||||
*/
|
||||
setSize(size) {
|
||||
this.dom.setAttributeNS(null, 'width', size[0])
|
||||
this.dom.setAttributeNS(null, 'height', size[1])
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if `t` has defined type and is not null.
|
||||
*/
|
||||
function ok(t) {
|
||||
return t !== undefined && t !== null
|
||||
}
|
||||
|
||||
return Histogram
|
@ -0,0 +1,24 @@
|
||||
/**
|
||||
* Add script to the DOM.
|
||||
*/
|
||||
function loadScript(url, onload) {
|
||||
let script = document.createElement('script')
|
||||
script.onload = onload
|
||||
script.src = url
|
||||
|
||||
document.head.appendChild(script)
|
||||
}
|
||||
|
||||
/**
|
||||
* Add css style file to the DOM.
|
||||
*/
|
||||
function loadStyle(url, onload) {
|
||||
let style = document.createElement('link')
|
||||
style.onload = onload
|
||||
style.href = url
|
||||
style.rel = 'stylesheet'
|
||||
style.media = 'screen'
|
||||
style.type = 'text/css'
|
||||
|
||||
document.head.appendChild(style)
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -1,86 +1,96 @@
|
||||
class TableViewVisualization extends Visualization {
|
||||
static inputType = "Any"
|
||||
static inputType = 'Any'
|
||||
|
||||
onDataReceived(data) {
|
||||
function tableOf(content, level) {
|
||||
let open = '<table class="level' + level + '">';
|
||||
return open + content + "</table>";
|
||||
let open = '<table class="level' + level + '">'
|
||||
return open + content + '</table>'
|
||||
}
|
||||
|
||||
function hasExactlyKeys(keys, obj) {
|
||||
return Object.keys(obj).length === keys.length && keys.every(k => obj.hasOwnProperty(k));
|
||||
return (
|
||||
Object.keys(obj).length === keys.length &&
|
||||
keys.every((k) => obj.hasOwnProperty(k))
|
||||
)
|
||||
}
|
||||
|
||||
function getAtNestedKey(data, key) {
|
||||
let res = data;
|
||||
key.forEach(k => res = res[k]);
|
||||
return res;
|
||||
let res = data
|
||||
key.forEach((k) => (res = res[k]))
|
||||
return res
|
||||
}
|
||||
|
||||
function repNestedKey(key) {
|
||||
return key.join(".");
|
||||
return key.join('.')
|
||||
}
|
||||
|
||||
function generateNestings(data, key) {
|
||||
let first = getAtNestedKey(data[0], key);
|
||||
if (!(first instanceof Object)) return [key];
|
||||
let firstKeys = Object.keys(first);
|
||||
let isNestable = data.every(obj => hasExactlyKeys(firstKeys, getAtNestedKey(obj, key)));
|
||||
let first = getAtNestedKey(data[0], key)
|
||||
if (!(first instanceof Object)) return [key]
|
||||
let firstKeys = Object.keys(first)
|
||||
let isNestable = data.every((obj) =>
|
||||
hasExactlyKeys(firstKeys, getAtNestedKey(obj, key))
|
||||
)
|
||||
if (isNestable) {
|
||||
let withNests = firstKeys.map(k => key.concat([k]));
|
||||
let furtherNestings = withNests.map(k => generateNestings(data, k));
|
||||
return [].concat.apply([], furtherNestings);
|
||||
let withNests = firstKeys.map((k) => key.concat([k]))
|
||||
let furtherNestings = withNests.map((k) =>
|
||||
generateNestings(data, k)
|
||||
)
|
||||
return [].concat.apply([], furtherNestings)
|
||||
} else {
|
||||
return [key];
|
||||
return [key]
|
||||
}
|
||||
}
|
||||
|
||||
function isObjectMatrix(data) {
|
||||
let isList = Array.isArray(data) && data[0];
|
||||
if (!isList || !(typeof data[0] === "object")) return false;
|
||||
let firstKeys = Object.keys(data[0]);
|
||||
return data.every(obj => hasExactlyKeys(firstKeys, obj));
|
||||
let isList = Array.isArray(data) && data[0]
|
||||
if (!isList || !(typeof data[0] === 'object')) return false
|
||||
let firstKeys = Object.keys(data[0])
|
||||
return data.every((obj) => hasExactlyKeys(firstKeys, obj))
|
||||
}
|
||||
|
||||
function genObjectMatrix(data, level) {
|
||||
let result = "<tr><th></th>";
|
||||
let keys = Object.keys(data[0]);
|
||||
let nests = [].concat.apply([], keys.map(k => generateNestings(data,[k])));
|
||||
nests.forEach(key => {
|
||||
result += ("<th>" + repNestedKey(key) + "</th>");
|
||||
});
|
||||
result += "</tr>";
|
||||
let result = '<tr><th></th>'
|
||||
let keys = Object.keys(data[0])
|
||||
let nests = [].concat.apply(
|
||||
[],
|
||||
keys.map((k) => generateNestings(data, [k]))
|
||||
)
|
||||
nests.forEach((key) => {
|
||||
result += '<th>' + repNestedKey(key) + '</th>'
|
||||
})
|
||||
result += '</tr>'
|
||||
data.forEach((row, ix) => {
|
||||
result += ("<tr><th>" + ix + "</th>");
|
||||
nests.forEach(k => {
|
||||
result += toTableCell(getAtNestedKey(row, k), level);
|
||||
});
|
||||
result += ("</tr>")
|
||||
});
|
||||
return tableOf(result, level);
|
||||
result += '<tr><th>' + ix + '</th>'
|
||||
nests.forEach((k) => {
|
||||
result += toTableCell(getAtNestedKey(row, k), level)
|
||||
})
|
||||
result += '</tr>'
|
||||
})
|
||||
return tableOf(result, level)
|
||||
}
|
||||
|
||||
function isMatrix(data) {
|
||||
let isList = Array.isArray(data) && data[0];
|
||||
if (!isList) return false;
|
||||
let firstIsArray = Array.isArray(data[0]);
|
||||
if (!firstIsArray) return false;
|
||||
let firstLen = data[0].length;
|
||||
return data.every(d => d.length === firstLen);
|
||||
let isList = Array.isArray(data) && data[0]
|
||||
if (!isList) return false
|
||||
let firstIsArray = Array.isArray(data[0])
|
||||
if (!firstIsArray) return false
|
||||
let firstLen = data[0].length
|
||||
return data.every((d) => d.length === firstLen)
|
||||
}
|
||||
|
||||
function genMatrix(data, level, header) {
|
||||
let result = "<tr><th></th>";
|
||||
let result = '<tr><th></th>'
|
||||
if (header) {
|
||||
header.forEach((elt, ix) => {
|
||||
result += ("<th>" + elt + "</th>");
|
||||
});
|
||||
result += '<th>' + elt + '</th>'
|
||||
})
|
||||
} else {
|
||||
data[0].forEach((elt, ix)=> {
|
||||
result += ("<th>" + ix + "</th>");
|
||||
});
|
||||
data[0].forEach((elt, ix) => {
|
||||
result += '<th>' + ix + '</th>'
|
||||
})
|
||||
}
|
||||
result += "</tr>";
|
||||
result += '</tr>'
|
||||
table = []
|
||||
|
||||
data.forEach((d, i) => {
|
||||
@ -91,61 +101,70 @@ class TableViewVisualization extends Visualization {
|
||||
})
|
||||
|
||||
table.forEach((row, ix) => {
|
||||
result += ("<tr><th>" + ix + "</th>");
|
||||
result += '<tr><th>' + ix + '</th>'
|
||||
row.forEach((d) => {
|
||||
result += toTableCell(d, level);
|
||||
});
|
||||
result += ("</tr>")
|
||||
});
|
||||
return tableOf(result, level);
|
||||
result += toTableCell(d, level)
|
||||
})
|
||||
result += '</tr>'
|
||||
})
|
||||
return tableOf(result, level)
|
||||
}
|
||||
|
||||
function genGenericTable(data, level) {
|
||||
let result = "";
|
||||
let result = ''
|
||||
data.forEach((point, ix) => {
|
||||
result += ("<tr><th>" + ix + "</th>" + toTableCell(point, level) + "</tr>");
|
||||
});
|
||||
return tableOf(result, level);
|
||||
result +=
|
||||
'<tr><th>' +
|
||||
ix +
|
||||
'</th>' +
|
||||
toTableCell(point, level) +
|
||||
'</tr>'
|
||||
})
|
||||
return tableOf(result, level)
|
||||
}
|
||||
|
||||
function genRowObjectTable(data, level) {
|
||||
let keys = Object.keys(data);
|
||||
let result = "<tr>";
|
||||
keys.forEach(key => {
|
||||
result += ("<th>" + key + "</th>");
|
||||
});
|
||||
result += "</tr><tr>";
|
||||
keys.forEach(key => {
|
||||
result += toTableCell(data[key], level);
|
||||
});
|
||||
result += "</tr>";
|
||||
return tableOf(result, level);
|
||||
let keys = Object.keys(data)
|
||||
let result = '<tr>'
|
||||
keys.forEach((key) => {
|
||||
result += '<th>' + key + '</th>'
|
||||
})
|
||||
result += '</tr><tr>'
|
||||
keys.forEach((key) => {
|
||||
result += toTableCell(data[key], level)
|
||||
})
|
||||
result += '</tr>'
|
||||
return tableOf(result, level)
|
||||
}
|
||||
|
||||
function toTableCell(data, level) {
|
||||
if (Array.isArray(data)) {
|
||||
return "<td>" + genTable(data, level + 1) + "</td>";
|
||||
return '<td>' + genTable(data, level + 1) + '</td>'
|
||||
} else if (data instanceof Object) {
|
||||
return "<td>" + genRowObjectTable(data, level + 1) + "</td>";
|
||||
return '<td>' + genRowObjectTable(data, level + 1) + '</td>'
|
||||
} else {
|
||||
if (data === undefined || data === null) data = "";
|
||||
let res = data.toString();
|
||||
return '<td class="plaintext">' + (res === "" ? "N/A" : res) + '</td>';
|
||||
if (data === undefined || data === null) data = ''
|
||||
let res = data.toString()
|
||||
return (
|
||||
'<td class="plaintext">' +
|
||||
(res === '' ? 'N/A' : res) +
|
||||
'</td>'
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function genTable(data, level, header) {
|
||||
if (isMatrix(data)) {
|
||||
return genMatrix(data, level, header);
|
||||
return genMatrix(data, level, header)
|
||||
} else if (isObjectMatrix(data)) {
|
||||
return genObjectMatrix(data, level);
|
||||
return genObjectMatrix(data, level)
|
||||
} else {
|
||||
return genGenericTable(data, level);
|
||||
return genGenericTable(data, level)
|
||||
}
|
||||
}
|
||||
|
||||
while (this.dom.firstChild) {
|
||||
this.dom.removeChild(this.dom.lastChild);
|
||||
this.dom.removeChild(this.dom.lastChild)
|
||||
}
|
||||
|
||||
const style_dark = `
|
||||
@ -181,7 +200,7 @@ class TableViewVisualization extends Visualization {
|
||||
background-color: rgba(255, 255, 255, 0.03);
|
||||
}
|
||||
</style>
|
||||
`;
|
||||
`
|
||||
|
||||
const style_light = `
|
||||
<style>
|
||||
@ -217,39 +236,42 @@ class TableViewVisualization extends Visualization {
|
||||
}
|
||||
</style>`
|
||||
|
||||
const width = this.dom.getAttributeNS(null, "width");
|
||||
const height = this.dom.getAttributeNS(null, "height");
|
||||
const tabElem = document.createElement("div");
|
||||
tabElem.setAttributeNS(null,"id" ,"vis-tbl-view");
|
||||
tabElem.setAttributeNS(null,"class" ,"scrollable");
|
||||
tabElem.setAttributeNS(null,"viewBox","0 0 " + width + " " + height);
|
||||
tabElem.setAttributeNS(null,"width" ,"100%");
|
||||
tabElem.setAttributeNS(null,"height" ,"100%");
|
||||
const tblViewStyle =
|
||||
`width: ${width-10}px;
|
||||
height: ${height-10}px;
|
||||
const width = this.dom.getAttributeNS(null, 'width')
|
||||
const height = this.dom.getAttributeNS(null, 'height')
|
||||
const tabElem = document.createElement('div')
|
||||
tabElem.setAttributeNS(null, 'id', 'vis-tbl-view')
|
||||
tabElem.setAttributeNS(null, 'class', 'scrollable')
|
||||
tabElem.setAttributeNS(null, 'viewBox', '0 0 ' + width + ' ' + height)
|
||||
tabElem.setAttributeNS(null, 'width', '100%')
|
||||
tabElem.setAttributeNS(null, 'height', '100%')
|
||||
const tblViewStyle = `width: ${width - 10}px;
|
||||
height: ${height - 10}px;
|
||||
overflow: scroll;
|
||||
padding:2.5px;`;
|
||||
tabElem.setAttributeNS(null,"style" ,tblViewStyle);
|
||||
this.dom.appendChild(tabElem);
|
||||
padding:2.5px;`
|
||||
tabElem.setAttributeNS(null, 'style', tblViewStyle)
|
||||
this.dom.appendChild(tabElem)
|
||||
|
||||
let parsedData = data;
|
||||
if (typeof data === "string") {
|
||||
parsedData = JSON.parse(data);
|
||||
let parsedData = data
|
||||
if (typeof data === 'string') {
|
||||
parsedData = JSON.parse(data)
|
||||
}
|
||||
|
||||
let style = style_light
|
||||
if (document.getElementById("root").classList.contains("dark-theme")){
|
||||
style = style_dark;
|
||||
if (document.getElementById('root').classList.contains('dark-theme')) {
|
||||
style = style_dark
|
||||
}
|
||||
let table = genTable(parsedData.data || parsedData, 0, parsedData.header);
|
||||
tabElem.innerHTML = style + table;
|
||||
let table = genTable(
|
||||
parsedData.data || parsedData,
|
||||
0,
|
||||
parsedData.header
|
||||
)
|
||||
tabElem.innerHTML = style + table
|
||||
}
|
||||
|
||||
setSize(size) {
|
||||
this.dom.setAttributeNS(null, "width", size[0]);
|
||||
this.dom.setAttributeNS(null, "height", size[1]);
|
||||
this.dom.setAttributeNS(null, 'width', size[0])
|
||||
this.dom.setAttributeNS(null, 'height', size[1])
|
||||
}
|
||||
}
|
||||
|
||||
return TableViewVisualization;
|
||||
return TableViewVisualization
|
||||
|
@ -1,7 +1,7 @@
|
||||
export class Visualization {
|
||||
constructor(api) {
|
||||
this.dom = api.root();
|
||||
this.__api__ = api;
|
||||
this.dom = api.root()
|
||||
this.__api__ = api
|
||||
}
|
||||
setPreprocessor(code) {
|
||||
this.__api__.emit_preprocessor_change(code)
|
||||
|
@ -41,6 +41,7 @@ impl Registry {
|
||||
registry.add(builtin::visualization::native::RawText::definition());
|
||||
registry.try_add_java_script(builtin::visualization::java_script::bubble_visualization());
|
||||
registry.try_add_java_script(builtin::visualization::java_script::scatter_plot_visualization());
|
||||
registry.try_add_java_script(builtin::visualization::java_script::histogram_visualization());
|
||||
registry.try_add_java_script(builtin::visualization::java_script::table_view_visualization());
|
||||
registry.try_add_java_script(builtin::visualization::java_script::map_view_visualization());
|
||||
registry
|
||||
|
Loading…
Reference in New Issue
Block a user