diff --git a/lib/index.js b/lib/index.js index 2587276c897..909b06e5619 100644 --- a/lib/index.js +++ b/lib/index.js @@ -28,6 +28,7 @@ Plotly.register([ require('./mesh3d'), require('./cone'), require('./streamtube'), + require('./isosurface'), require('./scattergeo'), require('./choropleth'), diff --git a/lib/isosurface.js b/lib/isosurface.js new file mode 100644 index 00000000000..7bc97ae239e --- /dev/null +++ b/lib/isosurface.js @@ -0,0 +1,11 @@ +/** +* Copyright 2012-2018, Plotly, Inc. +* All rights reserved. +* +* This source code is licensed under the MIT license found in the +* LICENSE file in the root directory of this source tree. +*/ + +'use strict'; + +module.exports = require('../src/traces/isosurface'); diff --git a/package-lock.json b/package-lock.json index ed8e620daca..cedbaf5daf2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4963,6 +4963,25 @@ } } }, + "gl-isosurface3d": { + "version": "git+https://github.com/gl-vis/gl-isosurface3d.git#b4a79af9986263bd2bf1a70cc6096e711d2365ee", + "requires": { + "barycentric": "1.0.1", + "colormap": "2.3.0", + "gl-buffer": "2.1.2", + "gl-mat4": "1.2.0", + "gl-shader": "4.2.1", + "gl-texture2d": "2.1.0", + "gl-vao": "1.3.0", + "glsl-specular-cook-torrance": "2.0.1", + "glslify": "6.1.1", + "ndarray": "1.0.18", + "normals": "1.1.0", + "polytope-closest-point": "1.0.0", + "simplicial-complex-contour": "1.0.2", + "typedarray-pool": "1.1.0" + } + }, "gl-line3d": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/gl-line3d/-/gl-line3d-1.1.2.tgz", diff --git a/package.json b/package.json index e43717b668a..a7c3137e614 100644 --- a/package.json +++ b/package.json @@ -70,6 +70,7 @@ "es6-promise": "^3.0.2", "fast-isnumeric": "^1.1.1", "font-atlas-sdf": "^1.3.3", + "gl-isosurface3d": "^0.5.0", "gl-cone3d": "^1.1.0", "gl-contour2d": "^1.1.4", "gl-error3d": "^1.0.7", diff --git a/src/traces/isosurface/attributes.js b/src/traces/isosurface/attributes.js new file mode 100644 index 00000000000..b8460bd4dc2 --- /dev/null +++ b/src/traces/isosurface/attributes.js @@ -0,0 +1,165 @@ +/** +* Copyright 2012-2018, Plotly, Inc. +* All rights reserved. +* +* This source code is licensed under the MIT license found in the +* LICENSE file in the root directory of this source tree. +*/ + +'use strict'; + +var colorscaleAttrs = require('../../components/colorscale/attributes'); +var colorbarAttrs = require('../../components/colorbar/attributes'); +var mesh3dAttrs = require('../mesh3d/attributes'); +var baseAttrs = require('../../plots/attributes'); + +var extendFlat = require('../../lib/extend').extendFlat; + +var attrs = { + x: { + valType: 'data_array', + role: 'info', + editType: 'calc+clearAxisTypes', + description: [ + 'Sets the x coordinates of the volume data' + ].join(' ') + }, + y: { + valType: 'data_array', + role: 'info', + editType: 'calc+clearAxisTypes', + description: [ + 'Sets the y coordinates of the volume data' + ].join(' ') + }, + z: { + valType: 'data_array', + role: 'info', + editType: 'calc+clearAxisTypes', + description: [ + 'Sets the z coordinates of the volume data' + ].join(' ') + }, + + value: { + valType: 'data_array', + role: 'info', + editType: 'calc', + description: 'Sets the intensity values of the volume data.' + }, + + isomin: { + valType: 'number', + role: 'info', + editType: 'calc', + description: 'Sets the minimum intensity bound of the isosurface.' + }, + + isomax: { + valType: 'number', + role: 'info', + editType: 'calc', + description: 'Sets the maximum intensity bound of the isosurface.' + }, + + xmin: { + valType: 'number', + role: 'info', + editType: 'calc', + description: 'Sets the minimum x bound of the isosurface.' + }, + + xmax: { + valType: 'number', + role: 'info', + editType: 'calc', + description: 'Sets the maximum x bound of the isosurface.' + }, + + ymin: { + valType: 'number', + role: 'info', + editType: 'calc', + description: 'Sets the minimum y bound of the isosurface.' + }, + + ymax: { + valType: 'number', + role: 'info', + editType: 'calc', + description: 'Sets the maximum y bound of the isosurface.' + }, + + zmin: { + valType: 'number', + role: 'info', + editType: 'calc', + description: 'Sets the minimum z bound of the isosurface.' + }, + + zmax: { + valType: 'number', + role: 'info', + editType: 'calc', + description: 'Sets the maximum z bound of the isosurface.' + }, + + smoothnormals: { + valType: 'boolean', + role: 'info', + editType: 'calc', + description: 'Smooth normals of the isosurface. By default this is set to true.' + }, + + isocaps: { + valType: 'boolean', + role: 'info', + editType: 'calc', + description: 'Whether to generate isocaps for the isosurface. By default this is set to true.' + }, + + color: { + valType: 'color', + role: 'style', + editType: 'calc', + description: [ + 'Sets the color of the isosurface.', + 'By default the isosurface color is computed from the colorscale.', + 'Isocaps still use the colorscale colors even with this set.' + ].join(' ') + }, + + text: { + valType: 'string', + role: 'info', + dflt: '', + arrayOk: true, + editType: 'calc', + description: [ + 'Sets the text elements associated with the isosurface points.', + 'If trace `hoverinfo` contains a *text* flag and *hovertext* is not set,', + 'these elements will be seen in the hover labels.' + ].join(' ') + } +}; + +extendFlat(attrs, colorscaleAttrs('', { + colorAttr: 'value', + showScaleDflt: true, + editTypeOverride: 'calc' +}), { + colorbar: colorbarAttrs +}); + +var fromMesh3d = ['opacity', 'lightposition', 'lighting']; +fromMesh3d.forEach(function(k) { + attrs[k] = mesh3dAttrs[k]; +}); + +attrs.hoverinfo = extendFlat({}, baseAttrs.hoverinfo, { + editType: 'calc', + flags: ['x', 'y', 'z', 'value', 'text', 'name'], + dflt: 'x+y+z+value+text+name' +}); + +module.exports = attrs; diff --git a/src/traces/isosurface/calc.js b/src/traces/isosurface/calc.js new file mode 100644 index 00000000000..4ad31d4866a --- /dev/null +++ b/src/traces/isosurface/calc.js @@ -0,0 +1,59 @@ +/** +* Copyright 2012-2018, Plotly, Inc. +* All rights reserved. +* +* This source code is licensed under the MIT license found in the +* LICENSE file in the root directory of this source tree. +*/ + +'use strict'; + +var colorscaleCalc = require('../../components/colorscale/calc'); + +module.exports = function calc(gd, trace) { + var value = trace.value; + var len = value.length; + var vMax = -Infinity; + var vMin = Infinity; + var i = 0; + + for(i = 0; i < len; i++) { + var v = value[i]; + vMax = Math.max(vMax, v); + vMin = Math.min(vMin, v); + } + + trace._vMax = vMax; + + colorscaleCalc(trace, [vMin, vMax], '', 'c'); + + var x = trace.x; + var y = trace.y; + var z = trace.z; + + var xMax = -Infinity; + var xMin = Infinity; + var yMax = -Infinity; + var yMin = Infinity; + var zMax = -Infinity; + var zMin = Infinity; + + for(i = 0; i < len; i++) { + var xx = x[i]; + xMax = Math.max(xMax, xx); + xMin = Math.min(xMin, xx); + + var yy = y[i]; + yMax = Math.max(yMax, yy); + yMin = Math.min(yMin, yy); + + var zz = z[i]; + zMax = Math.max(zMax, zz); + zMin = Math.min(zMin, zz); + } + + trace._xbnds = [xMin, xMax]; + trace._ybnds = [yMin, yMax]; + trace._zbnds = [zMin, zMax]; + +}; diff --git a/src/traces/isosurface/convert.js b/src/traces/isosurface/convert.js new file mode 100644 index 00000000000..50b34d4e454 --- /dev/null +++ b/src/traces/isosurface/convert.js @@ -0,0 +1,269 @@ +/** +* Copyright 2012-2018, Plotly, Inc. +* All rights reserved. +* +* This source code is licensed under the MIT license found in the +* LICENSE file in the root directory of this source tree. +*/ + +/* + Usage example: + + var width = 64 + var height = 64 + var depth = 64 + + var xs = [] + var ys = [] + var zs = [] + + var data = new Uint16Array(width*height*depth) + for (var z=0; z= last) { + if(p > last) { + xs.push(p); + } + last = p; + } else { + break; + } + } + return xs; +} + +// This should find the bounding box min +// edge index so that `value` lies at the edge or outside +// the range starting from xs[edge index]. +// That is, xs[edge index] >= value. +function findMinIndex(xs, value) { + for(var i = 0; i < xs.length; i++) { + if(xs[i] >= value) { + return i; + } + } + return xs.length; +} + +// This should find the bounding box max +// edge index so that `value` lies at the edge or outside +// the range ending at xs[edge index]. +// That is, xs[edge index] <= value. +function findMaxIndex(xs, value) { + for(var i = xs.length - 1; i >= 0; i--) { + if(xs[i] <= value) { + return i; + } + } + return -1; +} + +function toDataCoords(scene, arr, axisName) { + var sceneLayout = scene.fullSceneLayout; + var dataScale = scene.dataScale; + var ax = sceneLayout[axisName]; + var scale = dataScale[axisName2scaleIndex[axisName]]; + return simpleMap(arr, function(v) { return ax.d2l(v) * scale; }); +} + +function convert(scene, trace) { + var isosurfaceOpts = {}; + + var xs = getSequence(trace.x); + var ys = getSequence(trace.y); + var zs = getSequence(trace.z); + + isosurfaceOpts.dimensions = [xs.length, ys.length, zs.length]; + isosurfaceOpts.meshgrid = [ + toDataCoords(scene, xs, 'xaxis'), + toDataCoords(scene, ys, 'yaxis'), + toDataCoords(scene, zs, 'zaxis') + ]; + + + isosurfaceOpts.values = trace.value; + + if(trace.colorscale) { + isosurfaceOpts.colormap = parseColorScale(trace.colorscale); + } + if(trace.color) { + isosurfaceOpts.capsColormap = isosurfaceOpts.colormap; + var color = str2RgbaArray(trace.color).map(function(c) { return c * 255; }); + isosurfaceOpts.colormap = [{index: 0, rgb: color}, {index: 1, rgb: color}]; + if(!isosurfaceOpts.capsColormap) { + isosurfaceOpts.capsColormap = isosurfaceOpts.colormap; + } + } + isosurfaceOpts.vertexIntensityBounds = [trace.cmin, trace.cmax]; + isosurfaceOpts.isoBounds = [trace.isomin, trace.isomax]; + + isosurfaceOpts.isoCaps = trace.isocaps; + isosurfaceOpts.singleMesh = false; + + isosurfaceOpts.smoothNormals = trace.smoothnormals === undefined ? true : trace.smoothnormals; + + var bounds = [[0, 0, 0], isosurfaceOpts.dimensions.slice()]; + + if(trace.xmin !== undefined) { + bounds[0][0] = findMinIndex(xs, trace.xmin); + } + if(trace.ymin !== undefined) { + bounds[0][1] = findMinIndex(ys, trace.ymin); + } + if(trace.zmin !== undefined) { + bounds[0][2] = findMinIndex(zs, trace.zmin); + } + if(trace.xmax !== undefined) { + bounds[1][0] = findMaxIndex(xs, trace.xmax); + } + if(trace.ymax !== undefined) { + bounds[1][1] = findMaxIndex(ys, trace.ymax); + } + if(trace.zmax !== undefined) { + bounds[1][2] = findMaxIndex(zs, trace.zmax); + } + + var meshData = isosurfacePlot(isosurfaceOpts, bounds); + + // pass gl-mesh3d lighting attributes + var lp = trace.lightposition; + meshData.lightPosition = [lp.x, lp.y, lp.z]; + meshData.ambient = trace.lighting.ambient; + meshData.diffuse = trace.lighting.diffuse; + meshData.specular = trace.lighting.specular; + meshData.roughness = trace.lighting.roughness; + meshData.fresnel = trace.lighting.fresnel; + meshData.opacity = trace.opacity; + + return meshData; +} + +proto.update = function(data) { + this.data = data; + + var meshData = convert(this.scene, data); + this.mesh.update(meshData); +}; + +proto.dispose = function() { + this.scene.glplot.remove(this.mesh); + this.mesh.dispose(); +}; + +function createIsosurfaceTrace(scene, data) { + var gl = scene.glplot.gl; + + var meshData = convert(scene, data); + var mesh = isosurfacePlot.createTriMesh(gl, meshData); + var capMesh = isosurfacePlot.createTriMesh(gl, meshData.caps); + var trace = data; + var xbnds = toDataCoords(scene, trace._xbnds, 'xaxis'); + var ybnds = toDataCoords(scene, trace._ybnds, 'yaxis'); + var zbnds = toDataCoords(scene, trace._zbnds, 'zaxis'); + mesh.bounds = [ + [xbnds[0], ybnds[0], zbnds[0]], + [xbnds[1], ybnds[1], zbnds[1]], + ]; + + var isosurface = new Isosurface(scene, data.uid); + isosurface.mesh = mesh; + isosurface.data = data; + isosurface.meshData = meshData; + mesh._trace = isosurface; + + var caps = new Isosurface(scene, data.uid); + caps.mesh = capMesh; + caps.data = data; + caps.meshData = meshData; + capMesh._trace = caps; + + isosurface.caps = caps; + + scene.glplot.add(mesh); + scene.glplot.add(capMesh); + + return isosurface; +} + +module.exports = createIsosurfaceTrace; diff --git a/src/traces/isosurface/defaults.js b/src/traces/isosurface/defaults.js new file mode 100644 index 00000000000..7c0112f78b0 --- /dev/null +++ b/src/traces/isosurface/defaults.js @@ -0,0 +1,66 @@ +/** +* Copyright 2012-2018, Plotly, Inc. +* All rights reserved. +* +* This source code is licensed under the MIT license found in the +* LICENSE file in the root directory of this source tree. +*/ + + +'use strict'; + +var Lib = require('../../lib'); + +var colorscaleDefaults = require('../../components/colorscale/defaults'); +var attributes = require('./attributes'); + +module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout) { + function coerce(attr, dflt) { + return Lib.coerce(traceIn, traceOut, attributes, attr, dflt); + } + + var value = coerce('value'); + + var x = coerce('x'); + var y = coerce('y'); + var z = coerce('z'); + + if( + !value || !value.length || + !x || !x.length || !y || !y.length || !z || !z.length + ) { + traceOut.visible = false; + return; + } + + coerce('color'); + + coerce('isomin'); + coerce('isomax'); + coerce('smoothnormals'); + coerce('isocaps'); + + coerce('xmin'); + coerce('ymin'); + coerce('zmin'); + + coerce('xmax'); + coerce('ymax'); + coerce('zmax'); + + coerce('lighting.ambient'); + coerce('lighting.diffuse'); + coerce('lighting.specular'); + coerce('lighting.roughness'); + coerce('lighting.fresnel'); + coerce('lightposition.x'); + coerce('lightposition.y'); + coerce('lightposition.z'); + + colorscaleDefaults(traceIn, traceOut, layout, coerce, {prefix: '', cLetter: 'c'}); + + coerce('text'); + + // disable 1D transforms (for now) + traceOut._length = null; +}; diff --git a/src/traces/isosurface/index.js b/src/traces/isosurface/index.js new file mode 100644 index 00000000000..654e32812a8 --- /dev/null +++ b/src/traces/isosurface/index.js @@ -0,0 +1,35 @@ +/** +* Copyright 2012-2018, Plotly, Inc. +* All rights reserved. +* +* This source code is licensed under the MIT license found in the +* LICENSE file in the root directory of this source tree. +*/ + +'use strict'; + +module.exports = { + moduleType: 'trace', + name: 'isosurface', + basePlotModule: require('../../plots/gl3d'), + categories: ['gl3d'], + + attributes: require('./attributes'), + supplyDefaults: require('./defaults'), + colorbar: { + min: 'cmin', + max: 'cmax' + }, + calc: require('./calc'), + plot: require('./convert'), + + meta: { + description: [ + 'Use isosurfaces to visualize volumetric data.', + '', + 'Specify a volume using 4 1D arrays,', + '3 position arrays `x`, `y` and `z`', + 'and intensity array `u`.' + ].join(' ') + } +};