diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f9ee0bd01..0db47f9756 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,8 +11,8 @@ will be lost. In this build we added notification in statusbar to signalize that the connection was lost and IDE must be restarted. In future IDE will try to automatically reconnect. - - [Visualization can be extended to the whole screen][1355] by selecting the - node and pressing space twice. To quit this view, press space again. +- [Visualization can be extended to the whole screen][1355] by selecting the + node and pressing space twice. To quit this view, press space again. - [Visualization preview on output port hover.][1363] There is now a quick preview for visualizations and error descriptions. Hovering a node output will first show a tooltip with the type information and then after some time, will @@ -32,6 +32,7 @@ different color, shape and size, all as defined by the data within the `Table`. - [Many small visual improvements.][1419] See the source issue for more details. +- [Added Heatmap visualization.][1438]
![Bug Fixes](/docs/assets/tags/bug_fixes.svg) @@ -150,6 +151,7 @@ you can find their release notes [1419]: https://github.com/enso-org/ide/pull/1419 [1413]: https://github.com/enso-org/ide/pull/1413 [1428]: https://github.com/enso-org/ide/pull/1428 +[1438]: https://github.com/enso-org/ide/pull/1438
diff --git a/src/rust/ide/view/graph-editor/src/builtin/visualization/java_script.rs b/src/rust/ide/view/graph-editor/src/builtin/visualization/java_script.rs index f5cfb952e6..2b1e69e897 100644 --- a/src/rust/ide/view/graph-editor/src/builtin/visualization/java_script.rs +++ b/src/rust/ide/view/graph-editor/src/builtin/visualization/java_script.rs @@ -50,6 +50,16 @@ pub fn histogram_visualization() -> visualization::java_script::FallibleDefiniti visualization::java_script::Definition::new_builtin(source) } +/// Return a `JavaScript` Heatmap visualization. +pub fn heatmap_visualization() -> visualization::java_script::FallibleDefinition { + let loading_scripts = include_str!("java_script/helpers/loading.js"); + let number = include_str!("java_script/helpers/number.js"); + let source = include_str!("java_script/heatmap.js"); + let source = format!("{}{}{}",loading_scripts,number,source); + + visualization::java_script::Definition::new_builtin(source) +} + /// Return a `JavaScript` Map visualization. pub fn geo_map_visualization() -> visualization::java_script::FallibleDefinition { let loading_scripts = include_str!("java_script/helpers/loading.js"); diff --git a/src/rust/ide/view/graph-editor/src/builtin/visualization/java_script/heatmap.js b/src/rust/ide/view/graph-editor/src/builtin/visualization/java_script/heatmap.js new file mode 100644 index 0000000000..11ec16906e --- /dev/null +++ b/src/rust/ide/view/graph-editor/src/builtin/visualization/java_script/heatmap.js @@ -0,0 +1,208 @@ +/** Heatmap 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') + +/** + * A d3.js heatmap visualization. + */ +class Heatmap extends Visualization { + static inputType = 'Standard.Table.Data.Table.Table | Standard.Base.Data.Vector.Vector' + static label = 'Heatmap' + + constructor(data) { + super(data) + this.setPreprocessorModule('Standard.Visualization.Table.Visualization') + this.setPreprocessorCode(`x -> here.prepare_visualization x 1000`) + } + + onDataReceived(data) { + const { parsedData, isUpdate } = this.parseData(data) + if (!ok(parsedData)) { + console.error('Heatmap got invalid data: ' + data.toString()) + } + this.updateState(parsedData, isUpdate) + + if (!this.isInitialised()) { + this.initCanvas() + } + + this.initHeatmap() + } + + parseData(data) { + let parsedData + if (typeof data === 'string' || data instanceof String) { + parsedData = JSON.parse(data) + } else { + parsedData = data + } + const isUpdate = parsedData.update === 'diff' + return { parsedData, isUpdate } + } + + /** + * 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._dataValues = ok(data.data) ? data.data : this.data + } else { + this._dataValues = this.extractValues(data) + } + } + + extractValues(rawData) { + /// Note this is a workaround for #1006, we allow raw arrays and JSON objects to be consumed. + if (ok(rawData.data)) { + return rawData.data + } else if (Array.isArray(rawData)) { + return rawData + } + return [] + } + + /** + * Return vales to plot. + */ + data() { + return this._dataValues || {} + } + + /** + * 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') + const height = this.dom.getAttributeNS(null, 'height') + const margin = { top: 20, right: 20, bottom: 20, left: 25 } + return { + inner: { + width: width - margin.left - margin.right, + height: height - margin.top - margin.bottom, + }, + outer: { width, height }, + margin, + } + } + + /** + * Creates HTML div element as container for plot. + */ + createOuterContainerWithStyle(width, height) { + const divElem = document.createElementNS(null, 'div') + divElem.setAttributeNS(null, 'class', 'vis-heatmap') + divElem.setAttributeNS(null, 'viewBox', 0 + ' ' + 0 + ' ' + width + ' ' + height) + divElem.setAttributeNS(null, 'width', '100%') + divElem.setAttributeNS(null, 'height', '100%') + + return divElem + } + + /** + * 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 + ')' + ) + } + + /** + * Initialise the heatmap with the current data and settings. + */ + initHeatmap() { + let data = this.data() + // Labels of row and columns + let myGroups = d3.map(data[0], d => d).keys() + let myVars = d3.map(data[1], d => d).keys() + let labelStyle = 'font-family: DejaVuSansMonoBook; font-size: 10px;' + + // Build X scales and axis: + let x = d3.scaleBand().range([0, this.canvas.inner.width]).domain(myGroups).padding(0.05) + this.svg + .append('g') + .attr('style', labelStyle) + .attr('transform', 'translate(0,' + this.canvas.inner.height + ')') + .call(d3.axisBottom(x).tickSize(0)) + .select('.domain') + .remove() + + // Build Y scales and axis: + let y = d3.scaleBand().range([this.canvas.inner.height, 0]).domain(myVars).padding(0.05) + this.svg + .append('g') + .attr('style', labelStyle) + .call(d3.axisLeft(y).tickSize(0)) + .select('.domain') + .remove() + + // Build color scale + let fill = d3 + .scaleSequential() + .interpolator(d3.interpolateViridis) + .domain([0, d3.max(data[2], d => d)]) + + let indices = Array.from(Array(data[0].length).keys()) + + this.svg + .selectAll() + .data(indices, d => data[0][d] + ':' + data[1][d]) + .enter() + .append('rect') + .attr('x', d => x(data[0][d])) + .attr('y', d => y(data[1][d])) + .attr('rx', 4) + .attr('ry', 4) + .attr('width', x.bandwidth()) + .attr('height', y.bandwidth()) + .style('fill', d => fill(data[2][d])) + .style('stroke-width', 4) + .style('stroke', 'none') + .style('opacity', 0.8) + } + + /** + * 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 Heatmap diff --git a/src/rust/ide/view/graph-editor/src/component/visualization/registry.rs b/src/rust/ide/view/graph-editor/src/component/visualization/registry.rs index a009252383..c0359f695d 100644 --- a/src/rust/ide/view/graph-editor/src/component/visualization/registry.rs +++ b/src/rust/ide/view/graph-editor/src/component/visualization/registry.rs @@ -94,6 +94,7 @@ impl Registry { self.add(builtin::visualization::native::RawText::definition()); self.try_add_java_script(builtin::visualization::java_script::scatter_plot_visualization()); self.try_add_java_script(builtin::visualization::java_script::histogram_visualization()); + self.try_add_java_script(builtin::visualization::java_script::heatmap_visualization()); self.try_add_java_script(builtin::visualization::java_script::table_visualization()); self.try_add_java_script(builtin::visualization::java_script::sql_visualization()); self.try_add_java_script(builtin::visualization::java_script::geo_map_visualization());