Original commit: 51d9d5a749
This commit is contained in:
Maciej Mikołajek 2020-11-26 14:49:30 +01:00 committed by GitHub
parent 162f4b63ec
commit 5a6d920ffe
15 changed files with 1697 additions and 699 deletions

View File

@ -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
View File

@ -0,0 +1,2 @@
**/lib/*
**/target/*

4
gui/.prettierrc.toml Normal file
View File

@ -0,0 +1,4 @@
trailingComma = "es5"
tabWidth = 4
semi = false
singleQuote = true

View File

@ -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

View File

@ -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 }

View File

@ -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)
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)
}

View File

@ -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

View File

@ -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)

View File

@ -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