diff --git a/empress/support_files/js/barplot-layer.js b/empress/support_files/js/barplot-layer.js index 4b323f411..bf51bc1c6 100644 --- a/empress/support_files/js/barplot-layer.js +++ b/empress/support_files/js/barplot-layer.js @@ -246,7 +246,7 @@ define([ color: this.initialDefaultColorHex, change: function (newColor) { // To my knowledge, there isn't a straightforward way of - // getting an RGB array out of the "TinyColor" values passed in + // getting an RGB number out of the "TinyColor" values passed in // by Spectrum: see // https://bgrins.github.io/spectrum#details-acceptedColorInputs scope.defaultColor = Colorer.hex2RGB(newColor.toHexString()); diff --git a/empress/support_files/js/barplot-panel-handler.js b/empress/support_files/js/barplot-panel-handler.js index 398fd2209..05a6cc31b 100644 --- a/empress/support_files/js/barplot-panel-handler.js +++ b/empress/support_files/js/barplot-panel-handler.js @@ -136,8 +136,8 @@ define([ // next to each other). this.useBorders = false; - // Borders (when enabled) default to white. (This is an RGB array.) - this.borderColor = [1, 1, 1]; + // Borders (when enabled) default to white. (This is an RGB number.) + this.borderColor = Colorer.rgbToFloat([255, 255, 255]); // ... and to having a length of whatever the default barplot layer // length divided by 2 is :) @@ -264,17 +264,12 @@ define([ BarplotPanel.prototype.initBorderOptions = function () { var scope = this; - // this.borderColor is always a RGB array, for the sake of everyone's + // this.borderColor is always a RGB number, for the sake of everyone's // sanity. However, spectrum requires that the specified color is a hex // string: so we have to convert it to hex first here (only to later // convert it back to RGB on change events). Eesh! - // A SILLY NOTE: Apparently chroma.gl() modifies the input RGB array if - // you pass it in directly, converting it into a weird thing that - // is represented in the browser console as "(4) [255, 255, 255, - // undefined, _clipped: false, _unclipped: Array(3)]". Unpacking the - // input array using ... (as done here with this.borderColor) seems to - // avoid this problem. - var startingColor = chroma.gl(...this.borderColor).hex(); + var borderColor = Colorer.unpackColor(this.borderColor); + var startingColor = chroma.gl(...borderColor).hex(); $(this.borderColorPicker).spectrum({ color: startingColor, diff --git a/empress/support_files/js/colorer.js b/empress/support_files/js/colorer.js index bab68b85e..f4b4dcb54 100644 --- a/empress/support_files/js/colorer.js +++ b/empress/support_files/js/colorer.js @@ -327,17 +327,46 @@ define(["chroma", "underscore", "util"], function (chroma, _, util) { /** * Returns a mapping of unique field values to their corresponding colors, - * where each color is in RGB array format. + * where each color is in RGB number format. * * @return{Object} rgbMap An object mapping each item in * this.sortedUniqueValues to its assigned color. Each - * color is represented by an array of [R, G, B], where R, - * G, B are all floats scaled to within the range [0, 1]. + * color is represented by a number (see rgbToFloat()). */ Colorer.prototype.getMapRGB = function () { return _.mapObject(this.__valueToColor, Colorer.hex2RGB); }; + /** + * Compresses a color array of the form [red, green, blue], where each + * element is in the range of [0, 255], into a single number. + * + * @param{Array} rgb The color array. The element in the array must in the + * range of [0, 255]. + * + * @return{Number} the compressed color to be used in WebGl shaders + */ + Colorer.rgbToFloat = function (rgb) { + return rgb[0] + rgb[1] * 256 + rgb[2] * 256 * 256; + }; + + /** + * Uncompress a RGB color encoded as a float (eg the output of rgbToFloat). + * This is the same function found in the WebGl shaders. + * However, functions in WebGl shaders cannot be called by js functions and + * vice versa. + */ + Colorer.unpackColor = function (f) { + var color = []; + // red + color[0] = f % 256.0; + // green + color[1] = ((f - color[0]) / 256.0) % 256.0; + // blue + color[2] = (f - color[0] - 256.0 * color[1]) / 65536.0; + return color; + }; + /** * Returns a mapping of unique field values to their corresponding colors, * where each color is in hex format. @@ -379,17 +408,18 @@ define(["chroma", "underscore", "util"], function (chroma, _, util) { }; /** - * Converts a hex color to an RGB array suitable for use with WebGL. + * Converts a hex color to an RGB float suitable for use with WebGL. * * @param {String} hexString - * @return {Array} rgbArray + * @return {Float} rgb * @classmethod */ Colorer.hex2RGB = function (hexString) { // chroma(hexString).gl() returns an array with four components (RGBA // instead of RGB). The slice() here strips off the final (alpha) // element, which causes problems with Empress' drawing code. - return chroma(hexString).gl().slice(0, 3); + var rgb = chroma(hexString).rgb().slice(0, 3); + return Colorer.rgbToFloat(rgb); }; /** diff --git a/empress/support_files/js/drawer.js b/empress/support_files/js/drawer.js index d77ce64b8..fdbee8289 100644 --- a/empress/support_files/js/drawer.js +++ b/empress/support_files/js/drawer.js @@ -1,17 +1,26 @@ -define(["glMatrix", "Camera"], function (gl, Camera) { +define(["underscore", "glMatrix", "Camera"], function (_, gl, Camera) { // Shaders used in Drawer var vertShaderTxt = [ "precision mediump float;", "", "attribute vec2 vertPosition;", "uniform mat4 mvpMat;", - "attribute vec3 color;", + "attribute float color;", "varying vec3 c;", "uniform float pointSize;", "", + "vec3 unpackColor(float f) {", + " vec3 color;", + " color.r = mod(f, 256.0);", + " color.g = mod((f - color.r) / 256.0, 256.0);", + " color.b = (f - color.r - (256.0 * color.g)) / (65536.0);", + " // rgb are in range [0...255] but they need to be [0...1]", + " return color / 255.0;", + "}", + "", "void main()", "{", - " c = color;", + " c = unpackColor(color);", " gl_Position = mvpMat * vec4(vertPosition, 0.0, 1.0);", " gl_PointSize = pointSize;", "}", @@ -50,10 +59,11 @@ define(["glMatrix", "Camera"], function (gl, Camera) { this.treeContainer = document.getElementById("tree-container"); this.contex_ = canvas.getContext("webgl"); this.cam = cam; - this.VERTEX_SIZE = 5; + this.VERTEX_SIZE = 3; + this.COORD_SIZE = 2; // sets empress to light mode - this.CLR_COL = 1; + this.CLR_COL = [1, 1, 1]; // the center of the viewing window in tree coordinates this.treeSpaceCenterX = null; @@ -71,6 +81,9 @@ define(["glMatrix", "Camera"], function (gl, Camera) { this.SELECTED_NODE_CIRCLE_DIAMETER = 9.0; this.showTreeNodes = false; + + // the valid buffer types used in bindBuffer() + this.BUFF_TYPES = [1, 2, 3]; } /** @@ -90,7 +103,7 @@ define(["glMatrix", "Camera"], function (gl, Camera) { } // initialze canvas to have fully white background - c.clearColor(this.CLR_COL, this.CLR_COL, this.CLR_COL, 1); + c.clearColor(...this.CLR_COL, 1); c.clear(c.COLOR_BUFFER_BIT | c.DEPTH_BUFFER_BIT); // create webGL program @@ -111,9 +124,13 @@ define(["glMatrix", "Camera"], function (gl, Camera) { s.isSingle = c.getUniformLocation(s, "isSingle"); s.pointSize = c.getUniformLocation(s, "pointSize"); - // buffer object for tree - s.treeVertBuff = c.createBuffer(); - this.treeVertSize = 0; + // buffer object for tree coordinates + s.treeCoordBuff = c.createBuffer(); + this.treeCoordSize = 0; + + // buffer object to store tree color + s.treeColorBuff = c.createBuffer(); + this.treeColorSize = 0; // buffer object used to thicken node lines s.thickNodeBuff = c.createBuffer(); @@ -222,15 +239,24 @@ define(["glMatrix", "Camera"], function (gl, Camera) { /** * Binds the buffer so WebGL can use it. + * There are three types of buffers: + * type 1: contains both coordinates and color + * type 2: only coordinates + * type 3: only color * * @param {WebGLBuffer} buffer The Buffer to bind + * @param {Number} buffType The type of buffer to bind + * @param {Number} vertSize The size of the vertex for webgl */ - Drawer.prototype.bindBuffer = function (buffer) { - // defines constants for a vertex. A vertex is the form [x, y, r, g, b] - const COORD_SIZE = 2; + Drawer.prototype.bindBuffer = function (buffer, buffType, vertSize) { + if (!_.contains(this.BUFF_TYPES, buffType)) { + throw "Invalid buffer type"; + } + + // defines constants for a vertex. A vertex is the form [x, y, rgb] const COORD_OFFSET = 0; - const COLOR_SIZE = 3; - const COLOR_OFFSET = 2; + const COLOR_SIZE = 1; + const COLOR_OFFSET = buffType == 1 ? 2 : 0; var c = this.contex_; var s = this.sProg_; @@ -238,34 +264,49 @@ define(["glMatrix", "Camera"], function (gl, Camera) { // tell webGL which buffer to use c.bindBuffer(c.ARRAY_BUFFER, buffer); - c.vertexAttribPointer( - s.vertPosition, - COORD_SIZE, - c.FLOAT, - c.FALSE, - this.VERTEX_SIZE * Float32Array.BYTES_PER_ELEMENT, - COORD_OFFSET - ); + if (buffType == 1 || buffType == 2) { + c.vertexAttribPointer( + s.vertPosition, + this.COORD_SIZE, + c.FLOAT, + c.FALSE, + vertSize * Float32Array.BYTES_PER_ELEMENT, + COORD_OFFSET + ); + } - c.vertexAttribPointer( - s.color, - COLOR_SIZE, - c.FLOAT, - c.FALSE, - this.VERTEX_SIZE * Float32Array.BYTES_PER_ELEMENT, - COLOR_OFFSET * Float32Array.BYTES_PER_ELEMENT - ); + if (buffType == 1 || buffType == 3) { + c.vertexAttribPointer( + s.color, + COLOR_SIZE, + c.FLOAT, + c.FALSE, + vertSize * Float32Array.BYTES_PER_ELEMENT, + COLOR_OFFSET * Float32Array.BYTES_PER_ELEMENT + ); + } + }; + + /** + * Fills the buffer used to draw the tree. + * + * @param {Array} data The coordinates [x, y, ...] to fill treeCoordBuffr + */ + Drawer.prototype.loadTreeCoordsBuff = function (data) { + data = new Float32Array(data); + this.treeCoordSize = data.length / 2; + this.fillBufferData_(this.sProg_.treeCoordBuff, data); }; /** * Fills the buffer used to draw the tree * - * @param {Array} data The coordinate and color data to fill tree buffer + * @param {Array} data The color data to fill treeColorBuff */ - Drawer.prototype.loadTreeBuff = function (data) { + Drawer.prototype.loadTreeColorBuff = function (data) { data = new Float32Array(data); - this.treeVertSize = data.length / 5; - this.fillBufferData_(this.sProg_.treeVertBuff, data); + this.treeColorSize = data.length; + this.fillBufferData_(this.sProg_.treeColorBuff, data); }; /** @@ -275,7 +316,7 @@ define(["glMatrix", "Camera"], function (gl, Camera) { */ Drawer.prototype.loadThickNodeBuff = function (data) { data = new Float32Array(data); - this.thickNodeSize = data.length / 5; + this.thickNodeSize = data.length / this.VERTEX_SIZE; this.fillBufferData_(this.sProg_.thickNodeBuff, data); }; @@ -287,7 +328,7 @@ define(["glMatrix", "Camera"], function (gl, Camera) { */ Drawer.prototype.loadBarplotBuff = function (data) { data = new Float32Array(data); - this.barplotSize = data.length / 5; + this.barplotSize = data.length / this.VERTEX_SIZE; this.fillBufferData_(this.sProg_.barplotBuff, data); }; @@ -298,7 +339,7 @@ define(["glMatrix", "Camera"], function (gl, Camera) { */ Drawer.prototype.loadSelectedNodeBuff = function (data) { data = new Float32Array(data); - this.selectedNodeSize = data.length / 5; + this.selectedNodeSize = data.length / this.VERTEX_SIZE; this.fillBufferData_(this.sProg_.selectedNodeBuff, data); }; @@ -309,7 +350,7 @@ define(["glMatrix", "Camera"], function (gl, Camera) { */ Drawer.prototype.loadNodeBuff = function (data) { data = new Float32Array(data); - this.nodeSize = data.length / 5; + this.nodeSize = data.length / this.VERTEX_SIZE; this.fillBufferData_(this.sProg_.nodeVertBuff, data); }; @@ -320,7 +361,7 @@ define(["glMatrix", "Camera"], function (gl, Camera) { */ Drawer.prototype.loadCladeBuff = function (data) { data = new Float32Array(data); - this.cladeVertSize = data.length / 5; + this.cladeVertSize = data.length / this.VERTEX_SIZE; this.fillBufferData_(this.sProg_.cladeBuff, data); }; @@ -367,26 +408,27 @@ define(["glMatrix", "Camera"], function (gl, Camera) { // draw tree node circles, if requested if (this.showTreeNodes) { c.uniform1f(s.pointSize, this.NODE_CIRCLE_DIAMETER); - this.bindBuffer(s.nodeVertBuff); + this.bindBuffer(s.nodeVertBuff, 1, 3); c.drawArrays(c.POINTS, 0, this.nodeSize); } // draw selected node c.uniform1f(s.pointSize, this.SELECTED_NODE_CIRCLE_DIAMETER); - this.bindBuffer(s.selectedNodeBuff); + this.bindBuffer(s.selectedNodeBuff, 1, 3); c.drawArrays(gl.POINTS, 0, this.selectedNodeSize); c.uniform1i(s.isSingle, 0); - this.bindBuffer(s.treeVertBuff); - c.drawArrays(c.LINES, 0, this.treeVertSize); + this.bindBuffer(s.treeCoordBuff, 2, 2); + this.bindBuffer(s.treeColorBuff, 3, 1); + c.drawArrays(c.LINES, 0, this.treeCoordSize); - this.bindBuffer(s.thickNodeBuff); + this.bindBuffer(s.thickNodeBuff, 1, 3); c.drawArrays(c.TRIANGLES, 0, this.thickNodeSize); - this.bindBuffer(s.barplotBuff); + this.bindBuffer(s.barplotBuff, 1, 3); c.drawArrays(c.TRIANGLES, 0, this.barplotSize); - this.bindBuffer(s.cladeBuff); + this.bindBuffer(s.cladeBuff, 1, 3); c.drawArrays(c.TRIANGLES, 0, this.cladeVertSize); }; diff --git a/empress/support_files/js/empress.js b/empress/support_files/js/empress.js index 8f8a40c2a..5b0108c91 100644 --- a/empress/support_files/js/empress.js +++ b/empress/support_files/js/empress.js @@ -79,7 +79,7 @@ define([ * @type {Array} * The default color of the tree */ - this.DEFAULT_COLOR = [0.25, 0.25, 0.25]; + this.DEFAULT_COLOR = Colorer.rgbToFloat([64, 64, 64]); /** * @type {BPTree} @@ -91,9 +91,10 @@ define([ /** * Used to index into _treeData * @type {Object} + * @private */ this._tdToInd = { - // all nodes (non layout parameters) + // all nodes (non-layout parameters) color: 0, isColored: 1, visible: 2, @@ -122,7 +123,7 @@ define([ * @type {Number} * @private */ - this._numNonLayoutParams = 3; + this._numOfNonLayoutParams = 3; /** * @type {Array} @@ -306,7 +307,7 @@ define([ * right: , * deepest: , * length: , - * color: <[r,g,b]> + * color: * } * } */ @@ -368,7 +369,7 @@ define([ this._yrscf = data.yScalingFactor; for (i = 1; i <= this._tree.size; i++) { // remove old layout information - this._treeData[i].length = this._numNonLayoutParams; + this._treeData[i].length = this._numOfNonLayoutParams; // store new layout information this._treeData[i][this._tdToInd.xr] = data.xCoord[i]; @@ -388,7 +389,7 @@ define([ ); for (i = 1; i <= this._tree.size; i++) { // remove old layout information - this._treeData[i].length = this._numNonLayoutParams; + this._treeData[i].length = this._numOfNonLayoutParams; // store new layout information this._treeData[i][this._tdToInd.xc0] = data.x0[i]; @@ -412,13 +413,14 @@ define([ ); for (i = 1; i <= this._tree.size; i++) { // remove old layout information - this._treeData[i].length = this._numNonLayoutParams; + this._treeData[i].length = this._numOfNonLayoutParams; // store new layout information this._treeData[i][this._tdToInd.x2] = data.xCoord[i]; this._treeData[i][this._tdToInd.y2] = data.yCoord[i]; } } + this._drawer.loadTreeCoordsBuff(this.getTreeCoords()); this._computeMaxDisplacement(); }; @@ -437,7 +439,6 @@ define([ this.getLayoutInfo(); this.centerLayoutAvgPoint(); - // centerLayoutAvgPoint() calls drawTree(), so no need to call it here }; /** @@ -475,7 +476,7 @@ define([ * Draws the tree */ Empress.prototype.drawTree = function () { - this._drawer.loadTreeBuff(this.getCoords()); + this._drawer.loadTreeColorBuff(this.getTreeColor()); if (this.drawNodeCircles) { this._drawer.loadNodeBuff(this.getNodeCoords()); } else { @@ -487,135 +488,23 @@ define([ }; /** - * Exports a SVG image of the tree. - * - * @return {String} svg - */ - Empress.prototype.exportTreeSVG = function () { - return ExportUtil.exportTreeSVG(this, this._drawer); - }; - - /** - * Exports a SVG image of the active legends. - * - * Currently this just includes the legend used for tree coloring, but - * eventually this'll be expanded to include all the barplot legends as - * well. - * - * @return {String} svg - */ - Empress.prototype.exportLegendSVG = function () { - var legends = []; - if (!_.isNull(this._legend.legendType)) { - legends.push(this._legend); - } - // TODO: get legends from barplot panel, which should in turn get them - // from each of its barplot layers. For now, we just export the tree - // legend, since we don't support exporting barplots quite yet (soon!) - if (legends.length === 0) { - util.toastMsg("No active legends to export.", 5000); - return null; - } else { - return ExportUtil.exportLegendSVG(legends); - } - }; - - /** - * Exports a PNG image of the canvas. - * - * This works a bit differently from the SVG exporting functions -- instead - * of returning a string with the SVG, the specified callback will be - * called with the Blob representation of the PNG. See - * ExportUtil.exportTreePNG() for details. - * - * @param {Function} callback Function that will be called with a Blob - * representing the exported PNG image. - */ - Empress.prototype.exportTreePNG = function (callback) { - ExportUtil.exportTreePNG(this, this._canvas, callback); - }; - - /** - * Retrieves x coordinate of node in the current layout. - * - * @param {Number} node Postorder position of node. - * @return {Number} x coordinate of node. - */ - Empress.prototype.getX = function (node) { - var xname = "x" + this._layoutToCoordSuffix[this._currentLayout]; - return this.getNodeInfo(node, xname); - }; - - /** - * Retrieves y coordinate of node in the current layout. - * - * @param {Number} node Postorder position of node. - * @return {Number} y coordinate of node. - */ - Empress.prototype.getY = function (node) { - var yname = "y" + this._layoutToCoordSuffix[this._currentLayout]; - return this.getNodeInfo(node, yname); - }; - - /** - * Retrieves the node coordinate info (for drawing node circles). - * - * @return {Array} Node coordinate info, formatted like - * [x, y, red, green, blue, ...] for every node circle to - * be drawn. - */ - Empress.prototype.getNodeCoords = function () { - var tree = this._tree; - var coords = []; - - for (var node = 1; node <= tree.size; node++) { - if (!this.getNodeInfo(node, "visible")) { - continue; - } - // In the past, we only drew circles for nodes with an assigned - // name (i.e. where the name of a node was not null). Now, we - // just draw circles for all nodes. - coords.push( - this.getX(node), - this.getY(node), - ...this.getNodeInfo(node, "color") - ); - } - return new Float32Array(coords); - }; - - /** - * Returns the number of lines/triangles to approximate an arc/wedge given - * the total angle of the arc/wedge. + * Retrives the coordinate info of the tree. * - * @param {Number} totalAngle The total angle of the arc/wedge - * @return {Number} The number of lines/triangles to approximate the arc - * or wedge. - */ - Empress.prototype._numSampToApproximate = function (totalAngle) { - var numSamples = Math.floor(60 * Math.abs(totalAngle / Math.PI)); - return numSamples >= 2 ? numSamples : 2; - }; - - /** - * Retrieves the coordinate info of the tree. - * format of coordinate info: [x, y, red, green, blue, ...] + * We used to interlace the coordinate information with the color information + * i.e. [x1, y1, red1, green1, blue1, x2, y2, red2, green2, blue2,...] + * This was inefficient because tree coordinates do not change during most + * update operations (such as feature coloring). Thus, we split the + * coordinate information into two seperate buffers. One for tree + * tree coordinates and another for color. * * @return {Array} */ - Empress.prototype.getCoords = function () { + Empress.prototype.getTreeCoords = function () { var tree = this._tree; - - // The coordinates (and colors) of the tree's nodes. var coords = []; - // branch color - var color; - - var coords_index = 0; - var addPoint = function (x, y) { - coords.push(x, y, ...color); + coords.push(x, y); }; /* Draw a vertical line, if we're in rectangular layout mode. Note that @@ -628,7 +517,6 @@ define([ * root be the ONLY node in the tree. So this behavior is ok.) */ if (this._currentLayout === "Rectangular") { - color = this.getNodeInfo(tree.size, "color"); addPoint( this.getX(tree.size), this.getNodeInfo(tree.size, "lowestchildyr") @@ -651,9 +539,6 @@ define([ continue; } - // branch color - color = this.getNodeInfo(node, "color"); - if (this._currentLayout === "Rectangular") { /* Nodes in the rectangular layout can have up to two "parts": * a horizontal line, and a vertical line at the end of this @@ -741,16 +626,222 @@ define([ } } } else { - // Draw nodes for the unrooted layout. - // coordinate info for parent addPoint(this.getX(parent), this.getY(parent)); - // coordinate info for current nodeN addPoint(this.getX(node), this.getY(node)); } } return new Float32Array(coords); }; + Empress.prototype.getTreeColor = function () { + var tree = this._tree; + + var coords = []; + var color; + var addPoint = function () { + coords.push(color, color); + }; + + /* Draw a vertical line, if we're in rectangular layout mode. Note that + * we *don't* draw a horizontal line (with the branch length of the + * root) for the root node, even if it has a nonzero branch length; + * this could be modified in the future if desired. See #141 on GitHub. + * + * (The python code explicitly disallows trees with <= 1 nodes, so + * we're never going to be in the unfortunate situation of having the + * root be the ONLY node in the tree. So this behavior is ok.) + */ + if (this._currentLayout === "Rectangular") { + color = this.getNodeInfo(tree.size, "color"); + addPoint(); + } + // iterate through the tree in postorder, skip root + for (var node = 1; node < tree.size; node++) { + if (!this.getNodeInfo(node, "visible")) { + continue; + } + + // branch color + color = this.getNodeInfo(node, "color"); + + if (this._currentLayout === "Rectangular") { + /* Nodes in the rectangular layout can have up to two "parts": + * a horizontal line, and a vertical line at the end of this + * line. These parts are indicated below as AAA... and BBB..., + * respectively. (Child nodes are indicated by CCC...) + * + * BCCCCCCCCCCCC + * B + * AAAAAAAB + * B + * BCCCCCCCCCCCC + * + * All nodes except for the root are drawn with a horizontal + * line, and all nodes except for tips are drawn with a + * vertical line. + */ + // 1. Draw horizontal line (we're already skipping the root) + addPoint(); + // 2. Draw vertical line, if this is an internal node + if (this.getNodeInfo(node, "lowestchildyr") !== undefined) { + // skip if node is root of collapsed clade + if (this._collapsedClades.hasOwnProperty(node)) continue; + addPoint(); + } + } else if (this._currentLayout === "Circular") { + /* Same deal as above, except instead of a "vertical line" this + * time we draw an "arc". + */ + // 1. Draw line protruding from parent (we're already skipping + // the root so this is ok) + // + // Note that position info for this is stored as two sets of + // coordinates: (xc0, yc0) for start point, (xc1, yc1) for end + // point. The *c1 coordinates are explicitly associated with + // the circular layout so we can just use this.getX() / + // this.getY() for these coordinates. + addPoint(); + // 2. Draw arc, if this is an internal node (note again that + // we're skipping the root) + if ( + !this._tree.isleaf(this._tree.postorderselect(node)) && + !this._collapsedClades.hasOwnProperty(node) + ) { + // An arc will be created for all internal nodes. + // arcs are created by sampling up to 60 small lines along + // the arc spanned by rotating the line (arcx0, arcy0) + // arcendangle - arcstartangle radians. This will create an + // arc that starts at each internal node's rightmost child + // and ends on the leftmost child. + var arcDeltaAngle = + this.getNodeInfo(node, "arcendangle") - + this.getNodeInfo(node, "arcstartangle"); + var numSamples = this._numSampToApproximate(arcDeltaAngle); + for (var line = 0; line < numSamples; line++) { + addPoint(); + } + } + } else { + // Draw nodes for the unrooted layout. + // coordinate info for parent + addPoint(); + } + } + return new Float32Array(coords); + }; + + /** + * Creates an SVG string to export the current drawing + * Exports a SVG image of the tree. + * + * @return {String} svg + */ + Empress.prototype.exportTreeSVG = function () { + return ExportUtil.exportTreeSVG(this, this._drawer); + }; + + /** + * Exports a SVG image of the active legends. + * + * Currently this just includes the legend used for tree coloring, but + * eventually this'll be expanded to include all the barplot legends as + * well. + * + * @return {String} svg + */ + Empress.prototype.exportLegendSVG = function () { + var legends = []; + if (!_.isNull(this._legend.legendType)) { + legends.push(this._legend); + } + // TODO: get legends from barplot panel, which should in turn get them + // from each of its barplot layers. For now, we just export the tree + // legend, since we don't support exporting barplots quite yet (soon!) + if (legends.length === 0) { + util.toastMsg("No active legends to export.", 5000); + return null; + } else { + return ExportUtil.exportLegendSVG(legends); + } + }; + + /** + * Exports a PNG image of the canvas. + * + * This works a bit differently from the SVG exporting functions -- instead + * of returning a string with the SVG, the specified callback will be + * called with the Blob representation of the PNG. See + * ExportUtil.exportTreePNG() for details. + * + * @param {Function} callback Function that will be called with a Blob + * representing the exported PNG image. + */ + Empress.prototype.exportTreePNG = function (callback) { + ExportUtil.exportTreePNG(this, this._canvas, callback); + }; + + /** + * Retrieves x coordinate of node in the current layout. + * + * @param {Number} node Postorder position of node. + * @return {Number} x coordinate of node. + */ + Empress.prototype.getX = function (node) { + var xname = "x" + this._layoutToCoordSuffix[this._currentLayout]; + return this.getNodeInfo(node, xname); + }; + + /** + * Retrieves y coordinate of node in the current layout. + * + * @param {Number} node Postorder position of node. + * @return {Number} y coordinate of node. + */ + Empress.prototype.getY = function (node) { + var yname = "y" + this._layoutToCoordSuffix[this._currentLayout]; + return this.getNodeInfo(node, yname); + }; + + /** + * Retrieves the node coordinate info (for drawing node circles). + * + * @return {Array} Node coordinate info, formatted like + * [x, y, red, green, blue, ...] for every node circle to + * be drawn. + */ + Empress.prototype.getNodeCoords = function () { + var tree = this._tree; + var coords = []; + + for (var node = 1; node <= tree.size; node++) { + if (!this.getNodeInfo(node, "visible")) { + continue; + } + // In the past, we only drew circles for nodes with an assigned + // name (i.e. where the name of a node was not null). Now, we + // just draw circles for all nodes. + coords.push( + this.getX(node), + this.getY(node), + this.getNodeInfo(node, "color") + ); + } + return new Float32Array(coords); + }; + + /** + * Returns the number of lines/triangles to approximate an arc/wedge given + * the total angle of the arc/wedge. + * + * @param {Number} totalAngle The total angle of the arc/wedge + * @return {Number} The number of lines/triangles to approximate the arc + * or wedge. + */ + Empress.prototype._numSampToApproximate = function (totalAngle) { + var numSamples = Math.floor(60 * Math.abs(totalAngle / Math.PI)); + return numSamples >= 2 ? numSamples : 2; + }; + /** * Returns an Object describing circular layout angle information for a * node. @@ -949,18 +1040,18 @@ define([ Empress.prototype._addTriangleCoords = function (coords, corners, color) { // Triangle 1 coords.push(...corners.tL); - coords.push(...color); + coords.push(color); coords.push(...corners.bL); - coords.push(...color); + coords.push(color); coords.push(...corners.bR); - coords.push(...color); + coords.push(color); // Triangle 2 coords.push(...corners.tL); - coords.push(...color); + coords.push(color); coords.push(...corners.tR); - coords.push(...color); + coords.push(color); coords.push(...corners.bR); - coords.push(...color); + coords.push(color); }; /* Adds coordinate/color info for a vertical line for a given node in the @@ -1762,11 +1853,7 @@ define([ // we can just increase the displacement and leave it at that. // (This works out very well if this is the "outermost" border -- then // we really don't need to do anything.) - if ( - borderColor[0] === 1 && - borderColor[1] === 1 && - borderColor[2] === 1 - ) { + if (borderColor === Colorer.rgbToFloat(this._drawer.CLR_COL)) { return maxD; } // ... Otherwise, we actually have to go and create bars @@ -1839,7 +1926,7 @@ define([ for (group in observationsPerGroup) { obs = Array.from(observationsPerGroup[group]); - // convert hex string to rgb array + // convert hex string to rgb number var rgb = Colorer.hex2RGB(group); for (var i = 0; i < obs.length; i++) { @@ -2147,7 +2234,7 @@ define([ * @param{Object} obs Maps categories to the unique nodes to be colored for * each category. * @param{Object} cm Maps categories to the colors to color their nodes - * with. Colors should be represented as RGB arrays, for + * with. Colors should be represented as RGB number, for * example as is done in the color values of the output * of Colorer.getMapRGB(). */ @@ -2181,6 +2268,7 @@ define([ this._drawer.loadThickNodeBuff([]); this._drawer.loadCladeBuff([]); this._group = new Array(this._tree.size + 1).fill(-1); + this._drawer.loadTreeCoordsBuff(this.getTreeCoords()); }; /** @@ -2554,6 +2642,7 @@ define([ } } } + this._drawer.loadTreeCoordsBuff(this.getTreeCoords()); }; /** @@ -2589,7 +2678,7 @@ define([ // left - the tip with the smallest angle // right - the tip with the largest angle var addPoint = function (point) { - cladeBuffer.push(...point, ...color); + cladeBuffer.push(...point, color); }; var getCoords = function (node) { return [scope.getX(node), scope.getY(node)]; diff --git a/empress/support_files/js/export-util.js b/empress/support_files/js/export-util.js index f07a00975..7b608a417 100644 --- a/empress/support_files/js/export-util.js +++ b/empress/support_files/js/export-util.js @@ -1,4 +1,4 @@ -define(["underscore", "chroma"], function (_, chroma) { +define(["underscore", "chroma", "Colorer"], function (_, chroma, Colorer) { /** * Given a SVG string and min/max x/y positions, creates an exportable SVG. * @@ -48,8 +48,11 @@ define(["underscore", "chroma"], function (_, chroma) { * @param {Number} i * @return {String} */ - var getRGB = function (coords, i) { - return chroma.gl(coords[i + 2], coords[i + 3], coords[i + 4]).css(); + var getRGB = function (color, i) { + var c = _.map(Colorer.unpackColor(color[i]), function (rgb) { + return rgb / 255; + }); + return chroma.gl(...c).css(); }; var minX = Number.POSITIVE_INFINITY; @@ -59,40 +62,38 @@ define(["underscore", "chroma"], function (_, chroma) { var svg = "\n"; // create a line from x1,y1 to x2,y2 for every two consecutive - // coordinates. 5 array elements encode one coordinate: - // i=x, i+1=y, i+2=red, i+3=green, i+4=blue - var coords = empress.getCoords(); - for ( - var i = 0; - i + 2 * drawer.VERTEX_SIZE <= coords.length; - i += 2 * drawer.VERTEX_SIZE - ) { + // coordinates. Two buffers are used to encode one coordinate + + // position buffer:i=x, i+1=y + // format: [x1, y1, x2, y2, ...] + var coords = empress.getTreeCoords(); + // color buffer: i=rgb + // format: [rgb1, rgb2, ...] + var colors = empress.getTreeColor(); + var totalNodes = colors.length; + var coordIndx; + for (var node = 0; node + 2 <= totalNodes; node += 2) { + coordIndx = node * 2; + // NOTE: we negate the y coordinates in order to match the way the + // tree is drawn. See #334 on GitHub for discussion. + var x1 = coords[coordIndx]; + var y1 = -coords[coordIndx + 1]; + var x2 = coords[coordIndx + drawer.COORD_SIZE]; + var y2 = -coords[coordIndx + 1 + drawer.COORD_SIZE]; + var color = getRGB(colors, node); + // "normal" lines have a default color, // all other lines have a user defined thickness // All lines are defined using the information from the child node. - // So, if coords[i+2] == DEFAULT_COLOR then coords[i+2+5] will - // also be equal to DEFAULT_COLOR. Thus, we can save checking three - // array elements here. // TODO: instead, adjust line width based on a node's isColored // tree data attribute, in corner-case where dflt node color is // included in a color map. // (Also: I'm not confident that SVG stroke width and line width in // the Empress visualization are comparable, at least now?) var linewidth = 1 + empress._currentLineWidth; - if ( - coords[i + 2] == empress.DEFAULT_COLOR[0] && - coords[i + 3] == empress.DEFAULT_COLOR[1] && - coords[i + 4] == empress.DEFAULT_COLOR[2] - ) { + if (colors[node] == empress.DEFAULT_COLOR) { linewidth = 1; } - // NOTE: we negate the y coordinates in order to match the way the - // tree is drawn. See #334 on GitHub for discussion. - var x1 = coords[i]; - var y1 = -coords[i + 1]; - var x2 = coords[i + drawer.VERTEX_SIZE]; - var y2 = -coords[i + 1 + drawer.VERTEX_SIZE]; - var color = getRGB(coords, i); // Add the branch to the SVG svg += @@ -119,7 +120,7 @@ define(["underscore", "chroma"], function (_, chroma) { // create a circle for each node if (drawer.showTreeNodes) { - radius = drawer.NODE_CIRCLE_DIAMETER / 2; + var radius = drawer.NODE_CIRCLE_DIAMETER / 2; svg += "\n"; coords = empress.getNodeCoords(); for ( @@ -135,7 +136,7 @@ define(["underscore", "chroma"], function (_, chroma) { '" r="' + radius + '" style="fill:' + - getRGB(coords, i) + + getRGB(coords, i + 2) + '"/>\n'; } // The edge of the bounding box should coincide with the "end" of a diff --git a/empress/support_files/js/select-node-menu.js b/empress/support_files/js/select-node-menu.js index 1a69a1c7b..dc4c922eb 100644 --- a/empress/support_files/js/select-node-menu.js +++ b/empress/support_files/js/select-node-menu.js @@ -491,7 +491,8 @@ define(["underscore", "util"], function (_, util) { var node = nodeKeys[i]; var x = this.empress.getX(node); var y = this.empress.getY(node); - highlightedNodes.push(...[x, y, 0, 1, 0]); + // Add a green circle indicating the highlighted node(s) + highlightedNodes.push(...[x, y, 65280]); } // send the buffer array to the drawer diff --git a/empress/support_files/js/side-panel-handler.js b/empress/support_files/js/side-panel-handler.js index 9cf4ffb5c..12d87e3ab 100644 --- a/empress/support_files/js/side-panel-handler.js +++ b/empress/support_files/js/side-panel-handler.js @@ -250,6 +250,7 @@ define(["underscore", "Colorer", "util"], function (_, Colorer, util) { } var lw = util.parseAndValidateNum(lwInput); this.empress.thickenColoredNodes(lw); + this.empress.drawTree(); }; diff --git a/tests/test-barplots.js b/tests/test-barplots.js index 496926875..4417d9b4d 100644 --- a/tests/test-barplots.js +++ b/tests/test-barplots.js @@ -66,7 +66,7 @@ require([ test("Barplot panel border option initialization (incl. initBorderOptions)", function () { var empress = this.initTestEmpress(); - deepEqual(empress._barplotPanel.borderColor, [1, 1, 1]); + deepEqual(empress._barplotPanel.borderColor, 16777215); // Color picker should correctly default to white var obsColor = $(empress._barplotPanel.borderColorPicker) @@ -283,11 +283,7 @@ require([ // Default color (for feature metadata barplots) defaults to red, // a.k.a. the first "Classic QIIME Colors" color equal(layer1.initialDefaultColorHex, "#ff0000"); - // (this is a hacky way of checking each element of a RGB triple; for - // some reason QUnit complains when trying to compare arrays) - equal(layer1.defaultColor[0], 1); - equal(layer1.defaultColor[1], 0); - equal(layer1.defaultColor[2], 0); + equal(layer1.defaultColor, 255); // Default length defaults to, well, DEFAULT_LENGTH equal(layer1.defaultLength, BarplotLayer.DEFAULT_LENGTH); diff --git a/tests/test-circular-layout-computation.js b/tests/test-circular-layout-computation.js index bf707aa4e..f89d61d81 100644 --- a/tests/test-circular-layout-computation.js +++ b/tests/test-circular-layout-computation.js @@ -203,31 +203,34 @@ require(["jquery", "BPTree", "BiomTable", "Empress"], function ( }); test("Test Circular Layout Arc Computation", function () { - var coords = this.empress.getCoords(); + var coords = this.empress.getTreeCoords(); // NOTE: all node numbers are in reference to the postorder position // starting at 1. // check if line for node 1 is correct (tip) var node = 1; - equal(coords[(node - 1) * 10], -2); // start x position - equal(coords[(node - 1) * 10 + 1], 2); // start y position - equal(coords[(node - 1) * 10 + 5], -2); // end x position - equal(coords[(node - 1) * 10 + 6], 0); // end y position + equal(coords[node - 1], -2); // start x position + equal(coords[node - 1 + 1], 2); // start y position + equal(coords[node - 1 + 2], -2); // end x position + equal(coords[node - 1 + 3], 0); // end y position // check if line for node 3 is correct (internal) node = 3; - equal(coords[(node - 1) * 10], 0); // start x position - equal(coords[(node - 1) * 10 + 1], 1); // start y position - equal(coords[(node - 1) * 10 + 5], 0); // end x position - equal(coords[(node - 1) * 10 + 6], -1); // end y position + equal(coords[(node - 1) * 4], 0); // start x position + equal(coords[(node - 1) * 4 + 1], 1); // start y position + equal(coords[(node - 1) * 4 + 2], 0); // end x position + equal(coords[(node - 1) * 4 + 3], -1); // end y position // For the arc for node 3 start at (2,0) and ends at (-2, 0) // check if arc for node 3 is correct - ok(Math.abs(coords[30] - 2) < 1.0e-15); // start x arc position - ok(Math.abs(coords[31 - 0]) < 1.0e-15); //start y arc position - // prettier-ignore - ok(Math.abs(coords[625] - (-2)) < 1.0e-15); // end x arc position - ok(Math.abs(coords[626] - 0 < 1.0e-15)); // end y arc position + ok(Math.abs(coords[12] - 2) < 1.0e-15); // start x arc position + ok(Math.abs(coords[13]) < 1.0e-15); //start y arc position + // the arc for node 3 spans PI radians thus, the number of lines to + // approximate arc are 60*((radians spanned by arc) / PI) = 60 + // the above calculation can be found at + // Empress._numSampToApproximate() + ok(Math.abs(coords[250] + 2) < 1.0e-15); // end arc x + ok(Math.abs(coords[251]) < 1.0e-15); //end arc y }); }); }); diff --git a/tests/test-colorer.js b/tests/test-colorer.js index e583882d9..fbc46186d 100644 --- a/tests/test-colorer.js +++ b/tests/test-colorer.js @@ -234,22 +234,39 @@ require([ c = new Colorer("Dark2", eles); var rgbMap = c.getMapRGB(); for (var i = 0; i < 3; i++) { - var expRGB = chroma( - dark2palette[i % dark2palette.length] - ).rgb(); + var expRGB = chroma(dark2palette[i]).rgb(); // Convert expRGB from an array of 3 numbers in the range - // [0, 255] to an array of 3 numbers in the range [0, 1] scaled - // proportionally. - var scaledExpRGB = expRGB.map(function (x) { - return x / 255; - }); + // [0, 255] to a float. + var scaledExpRGB = Colorer.rgbToFloat(expRGB); + var obsRGB = rgbMap[eles[i]]; + // Check that individual R/G/B components are correct - for (var v = 0; v < 3; v++) { - equal(obsRGB[v], scaledExpRGB[v]); - } + equal(obsRGB, scaledExpRGB); } }); + test("Test rgbToFloat", function () { + // check red + var compressed = Colorer.rgbToFloat([1, 0, 0]); + equal(compressed, 1, "check compressed red"); + + // check green + compressed = Colorer.rgbToFloat([0, 1, 0]); + equal(compressed, 256, "check compressed green"); + + // check blue + compressed = Colorer.rgbToFloat([0, 0, 1]); + equal(compressed, 256 * 256); + }); + test("Test uncompress rgb", function () { + var redColor = Colorer.rgbToFloat([1, 0, 0]); + unpackedRed = Colorer.unpackColor(redColor); + deepEqual(unpackedRed, [1, 0, 0], "unpacked red"); + + var randColor = Colorer.rgbToFloat([47, 255, 52]); + var unpackedRand = Colorer.unpackColor(randColor); + deepEqual(unpackedRand, [47, 255, 52], "unpacked random"); + }); test("Test Colorer.getMapHex", function () { var eles = ["abc", "def", "ghi"]; // Analogous to the getColorRGB() test above but simpler @@ -276,11 +293,8 @@ require([ rgbmap = colorer.getMapRGB(); equal(_.keys(rgbmap).length, 1); equal(_.keys(rgbmap)[0], "abc"); - // Hack to check that the values here are approximately right. - // See https://stackoverflow.com/a/12830454/10730311 for details. - equal(rgbmap.abc[0].toFixed(2), 0.89); - equal(rgbmap.abc[1].toFixed(2), 0.1); - equal(rgbmap.abc[2].toFixed(2), 0.11); + + equal(rgbmap.abc, 1841892); hexmap = colorer.getMapHex(); equal(_.keys(hexmap).length, 1); @@ -297,9 +311,7 @@ require([ rgbmap = colorer.getMapRGB(); equal(_.keys(rgbmap).length, 1); equal(_.keys(rgbmap)[0], "abc"); - equal(rgbmap.abc[0].toFixed(2), 0.27); - equal(rgbmap.abc[1].toFixed(2), 0.0); - equal(rgbmap.abc[2].toFixed(2), 0.33); + equal(rgbmap.abc, 5505348); hexmap = colorer.getMapHex(); equal(_.keys(hexmap).length, 1); @@ -461,48 +473,27 @@ require([ }); test("Test Colorer.hex2RGB", function () { // Red - deepEqual(Colorer.hex2RGB("#ff0000"), [1, 0, 0]); - deepEqual(Colorer.hex2RGB("#f00"), [1, 0, 0]); + deepEqual(Colorer.hex2RGB("#ff0000"), 255); + deepEqual(Colorer.hex2RGB("#f00"), 255); // Green - deepEqual(Colorer.hex2RGB("#00ff00"), [0, 1, 0]); - deepEqual(Colorer.hex2RGB("#0f0"), [0, 1, 0]); + deepEqual(Colorer.hex2RGB("#00ff00"), 65280); + deepEqual(Colorer.hex2RGB("#0f0"), 65280); // Blue - deepEqual(Colorer.hex2RGB("#0000ff"), [0, 0, 1]); - deepEqual(Colorer.hex2RGB("#00f"), [0, 0, 1]); + deepEqual(Colorer.hex2RGB("#0000ff"), 16711680); + deepEqual(Colorer.hex2RGB("#00f"), 16711680); // Ugly fuchsia - deepEqual(Colorer.hex2RGB("#f0f"), [1, 0, 1]); + deepEqual(Colorer.hex2RGB("#f0f"), 16711935); // Black - deepEqual(Colorer.hex2RGB("#000"), [0, 0, 0]); - deepEqual(Colorer.hex2RGB("#000000"), [0, 0, 0]); + deepEqual(Colorer.hex2RGB("#000"), 0); + deepEqual(Colorer.hex2RGB("#000000"), 0); // White - deepEqual(Colorer.hex2RGB("#fff"), [1, 1, 1]); - deepEqual(Colorer.hex2RGB("#ffffff"), [1, 1, 1]); + deepEqual(Colorer.hex2RGB("#fff"), 16777215); + deepEqual(Colorer.hex2RGB("#ffffff"), 16777215); - // For checking less "easy" colors, we round off both - // the observed and expected color channels to a reasonable - // precision (4 places after the decimal point). - // For reference, Chroma.js' docs -- when showing GL color arrays - // -- only seem to use 2 places after the decimal point, so this - // should be fine. - var checkColorApprox = function (obsColorArray, expColorArray) { - for (var i = 0; i < 3; i++) { - var obsChannel = obsColorArray[0].toFixed(4); - var expChannel = expColorArray[0].toFixed(4); - deepEqual(obsChannel, expChannel); - } - }; // QIIME orange (third value in the Classic QIIME Colors map) - checkColorApprox(Colorer.hex2RGB("#f27304"), [ - 0.949019, - 0.45098, - 0.015686, - ]); + equal(Colorer.hex2RGB("#f27304"), 291826); // QIIME purple (fifth color in the Classic QIIME Colors map) - checkColorApprox(Colorer.hex2RGB("#91278d"), [ - 0.568627, - 0.152941, - 0.552941, - ]); + equal(Colorer.hex2RGB("#91278d"), 9250705); }); test("Test Colorer.getGradientSVG (all numeric values)", function () { var eles = ["0", "1", "2", "3", "4"]; diff --git a/tests/test-empress.js b/tests/test-empress.js index 1716f236e..da2426769 100644 --- a/tests/test-empress.js +++ b/tests/test-empress.js @@ -62,14 +62,14 @@ require([ // #348, we still want to draw a circle for it. // prettier-ignore var rectCoords = new Float32Array([ - 1, 2, 0.75, 0.75, 0.75, - 3, 4, 0.75, 0.75, 0.75, - 5, 6, 0.75, 0.75, 0.75, - 7, 8, 0.75, 0.75, 0.75, - 9, 10, 0.75, 0.75, 0.75, + 1, 2, 3289650, + 3, 4, 3289650, + 5, 6, 3289650, + 7, 8, 3289650, + 9, 10, 3289650, // This next row contains coordinate data for node 6 - 11, 12, 0.75, 0.75, 0.75, - 13, 14, 0.75, 0.75, 0.75, + 11, 12, 3289650, + 13, 14, 3289650, ]); this.empress._currentLayout = "Rectangular"; var empressRecCoords = this.empress.getNodeCoords(); @@ -77,13 +77,13 @@ require([ // prettier-ignore var circCoords = new Float32Array([ - 15, 16, 0.75, 0.75, 0.75, - 17, 18, 0.75, 0.75, 0.75, - 19, 20, 0.75, 0.75, 0.75, - 21, 22, 0.75, 0.75, 0.75, - 23, 24, 0.75, 0.75, 0.75, - 25, 26, 0.75, 0.75, 0.75, - 27, 28, 0.75, 0.75, 0.75, + 15, 16, 3289650, + 17, 18, 3289650, + 19, 20, 3289650, + 21, 22, 3289650, + 23, 24, 3289650, + 25, 26, 3289650, + 27, 28, 3289650, ]); this.empress._currentLayout = "Circular"; var empressCirCoords = this.empress.getNodeCoords(); @@ -91,13 +91,13 @@ require([ // prettier-ignore var unrootCoords = new Float32Array([ - 29, 30, 0.75, 0.75, 0.75, - 31, 32, 0.75, 0.75, 0.75, - 33, 34, 0.75, 0.75, 0.75, - 35, 36, 0.75, 0.75, 0.75, - 37, 38, 0.75, 0.75, 0.75, - 39, 40, 0.75, 0.75, 0.75, - 41, 42, 0.75, 0.75, 0.75, + 29, 30, 3289650, + 31, 32, 3289650, + 33, 34, 3289650, + 35, 36, 3289650, + 37, 38, 3289650, + 39, 40, 3289650, + 41, 42, 3289650, ]); this.empress._currentLayout = "Unrooted"; var empressUnrootCoords = this.empress.getNodeCoords(); @@ -114,7 +114,7 @@ require([ // the entire tree should be colored. sampleGroup contain all tips for (var node = 1; node <= 7; node++) { - deepEqual(this.empress.getNodeInfo(node, "color"), [1.0, 0, 0]); + deepEqual(this.empress.getNodeInfo(node, "color"), 255); } }); @@ -133,23 +133,11 @@ require([ this.empress.colorSampleGroups(sampleGroups); for (var node = 1; node <= 7; node++) { if (redNodes.has(node)) { - deepEqual(this.empress.getNodeInfo(node, "color"), [ - 1.0, - 0, - 0, - ]); + deepEqual(this.empress.getNodeInfo(node, "color"), 255); } else if (greeNodes.has(node)) { - deepEqual(this.empress.getNodeInfo(node, "color"), [ - 0, - 1.0, - 0, - ]); + deepEqual(this.empress.getNodeInfo(node, "color"), 65280); } else { - deepEqual(this.empress.getNodeInfo(node, "color"), [ - 0.75, - 0.75, - 0.75, - ]); + deepEqual(this.empress.getNodeInfo(node, "color"), 3289650); } } }); @@ -176,16 +164,9 @@ require([ var aGroupNodes = new Set([1, 3]); for (var node = 1; node <= 7; node++) { if (aGroupNodes.has(node)) { - deepEqual( - this.empress.getNodeInfo(node, "color"), - chroma(cm.a).gl().slice(0, 3) - ); + equal(this.empress.getNodeInfo(node, "color"), 255); } else { - deepEqual(this.empress.getNodeInfo(node, "color"), [ - 0.75, - 0.75, - 0.75, - ]); + equal(this.empress.getNodeInfo(node, "color"), 3289650); } } @@ -231,21 +212,17 @@ require([ if (group1.has(node)) { deepEqual( this.empress.getNodeInfo(node, "color"), - chroma(cm["1"]).gl().slice(0, 3), + 255, "node: " + node ); } else if (group2.has(node)) { deepEqual( this.empress.getNodeInfo(node, "color"), - chroma(cm["2"]).gl().slice(0, 3), + 16711680, "node: " + node ); } else { - deepEqual(this.empress.getNodeInfo(node, "color"), [ - 0.75, - 0.75, - 0.75, - ]); + deepEqual(this.empress.getNodeInfo(node, "color"), 3289650); } } @@ -286,21 +263,17 @@ require([ if (group1.has(node)) { deepEqual( this.empress.getNodeInfo(node, "color"), - chroma(cm["1"]).gl().slice(0, 3), + 255, "node: " + node ); } else if (group2.has(node)) { deepEqual( this.empress.getNodeInfo(node, "color"), - chroma(cm["2"]).gl().slice(0, 3), + 16711680, "node: " + node ); } else { - deepEqual(this.empress.getNodeInfo(node, "color"), [ - 0.75, - 0.75, - 0.75, - ]); + deepEqual(this.empress.getNodeInfo(node, "color"), 3289650); } } }); @@ -648,34 +621,22 @@ require([ var collapseClades = [ 35, 36, - 0.75, - 0.75, - 0.75, + 3289650, 33, 34, - 0.75, - 0.75, - 0.75, + 3289650, 31, 32, - 0.75, - 0.75, - 0.75, + 3289650, 35, 36, - 0.75, - 0.75, - 0.75, + 3289650, 33, 34, - 0.75, - 0.75, - 0.75, + 3289650, 33, 34, - 0.75, - 0.75, - 0.75, + 3289650, ]; deepEqual(this.empress._collapsedCladeBuffer, collapseClades); @@ -709,12 +670,12 @@ require([ left: 2, right: 3, deepest: 4, - color: [1, 1, 1], + color: 255, }; // manual set coorindate of nodes to make testing easier this.empress._treeData[1] = [ - [1, 1, 1], + 255, false, true, 0, @@ -728,7 +689,7 @@ require([ 0, ]; this.empress._treeData[2] = [ - [1, 1, 1], + 255, false, true, 1, @@ -742,7 +703,7 @@ require([ Math.PI / 4, ]; this.empress._treeData[3] = [ - [1, 1, 1], + 255, false, true, -1, @@ -756,7 +717,7 @@ require([ (3 * Math.PI) / 4, ]; this.empress._treeData[4] = [ - [1, 1, 1], + 255, false, true, 0, @@ -775,34 +736,22 @@ require([ var exp = [ 0, 1, - 1, - 1, - 1, + 255, 0, 5, + 255, 1, 1, - 1, - 1, - 1, - 1, - 1, - 1, + 255, 0, 1, - 1, - 1, - 1, + 255, 0, 5, - 1, - 1, - 1, + 255, -1, -1, - 1, - 1, - 1, + 255, ]; deepEqual(this.empress._collapsedCladeBuffer, exp); @@ -810,7 +759,7 @@ require([ this.empress._collapsedCladeBuffer = []; this.empress._currentLayout = "Rectangular"; this.empress.createCollapsedCladeShape(1); - exp = [1, 0, 1, 1, 1, 5, -1, 1, 1, 1, 5, 1, 1, 1, 1]; + exp = [1, 0, 255, 5, -1, 255, 5, 1, 255]; deepEqual(this.empress._collapsedCladeBuffer, exp); // check circular @@ -840,17 +789,17 @@ require([ sin = Math.sin(0); for (var line = 0; line < numSamp; line++) { // root of clade - exp.push(...[0, 1, 1, 1, 1]); + exp.push(...[0, 1, 255]); x = sX * cos - sY * sin; y = sX * sin + sY * cos; - exp.push(...[x, y, 1, 1, 1]); + exp.push(...[x, y, 255]); cos = Math.cos((line + 1) * deltaAngle); sin = Math.sin((line + 1) * deltaAngle); x = sX * cos - sY * sin; y = sX * sin + sY * cos; - exp.push(...[x, y, 1, 1, 1]); + exp.push(...[x, y, 255]); } deepEqual(this.empress._collapsedCladeBuffer, exp); }); @@ -862,7 +811,7 @@ require([ right: 3, deepest: 3, length: 3, - color: [0.75, 0.75, 0.75], + color: 3289650, }; deepEqual(this.empress._collapsedClades[4], exp); @@ -873,7 +822,7 @@ require([ this.empress._currentLayout = "Circular"; this.empress._collapseClade(4); exp = { - color: [0.75, 0.75, 0.75], + color: 3289650, deepest: 3, left: 2, length: 3, @@ -1183,23 +1132,23 @@ require([ this.empress.updateLayout("Circular"); var node = 1; var angleInfo = this.empress._getNodeAngleInfo(node, Math.PI / 2); - // The [0.25, 0.5, 0.75] is the GL color we use. (Mostly chosen - // here so that each R/G/B value is distinct; apparently this is a - // weird shade of blue when you draw it.) - this.empress._addCircularBarCoords(coords, 100, 200, angleInfo, [ - 0.25, - 0.5, - 0.75, - ]); + // The 255 is the GL color we use. + this.empress._addCircularBarCoords( + coords, + 100, + 200, + angleInfo, + 255 + ); // Each call of _addTriangleCoords() draws a rectangle with two // triangles. This involves adding 6 positions to coords, and since - // positions take up 5 spaces in an array here (x, y, r, g, b), + // positions take up 3 spaces in an array here (x, y, rgb), // and since _addCircularBarCoords() creates two rectangles (four - // triangles), coords should contain 6*5 = 30 * 2 = 60 + 1 = 61 + // triangles), coords should contain 6*3*2+1 = 18*2+1 = 36+1 = 37 // elements. (The + 1 is for the preexisting thing in the array -- // it's in this test just to check that the input coords array // is appended to, not overwritten.) - equal(coords.length, 61); + equal(coords.length, 37); // Check the actual coordinate values. // For reference, _addTriangleCoords() works by (given four @@ -1245,18 +1194,18 @@ require([ if (i === 0) { equal(v, "preexisting thing in the array"); } else { - // Which group of 5 elements (x, y, r, g, b) does this + // Which group of 3 elements (x, y, rgb) does this // value fall in? We can figure this out by taking the - // floor of i / 5. The 0th 5-tuple is tL in the lower - // rectangle (covering [1, 5]), the 1th 5-tuple is bL in + // floor of i / 3. The 0th 3-tuple is tL in the lower + // rectangle (covering [1, 5]), the 1th 3-tuple is bL in // the lower rectangle (covering [6, 10]), etc. - // (This isn't necessary to compute for R / G / B values - // since those are going to be the same at every point, but + // (This isn't necessary to compute for R / G / B value + // since it will be the same at every point, but // this is needed for figuring out what the expected value // should be for an x or y value. checkCoordVal() does the // hard work in figuring that out.) - var floor = Math.floor(i / 5); - switch (i % 5) { + var floor = Math.floor(i / 3); + switch (i % 3) { case 1: // x coordinate checkCoordVal(floor, v, true); @@ -1265,20 +1214,9 @@ require([ // y coordinate checkCoordVal(floor, v, false); break; - case 3: - // Red (constant) - equal(v, 0.25); - break; - case 4: - // Green (constant) - equal(v, 0.5); - break; case 0: - // Blue (constant) - // Note that although coords[0] is divisible by 5, - // it shouldn't be 0.75 since it's filled in with a - // preexisting value. - equal(v, 0.75); + // color (constant) + equal(v, 255); break; default: throw new Error( @@ -1295,7 +1233,7 @@ require([ var rx = 1; var by = 2; var ty = 3; - var color = [0.3, 0.8, 0.9]; + var color = 255; this.empress._addRectangularBarCoords( coords, lx, @@ -1304,24 +1242,24 @@ require([ ty, color ); - // Each point has 5 values (x, y, r, g, b) and there are 3 + // Each point has 3 values (x, y, rgb) and there are 3 // triangles (6 points) added, so the length of coords should now - // be 6 * 5 = 30. - equal(coords.length, 30); + // be 6 * 3 = 18. + equal(coords.length, 18); // We use a pared-down form of the iteration test used above in // testing _addCircularBarCoords(). This function is simpler (it // only calls _addTriangleCoords() once, so only one rectangle is // added), and there isn't a preexisting element in coords, so // checking things is much less involved. _.each(coords, function (v, i) { - var floor = Math.floor(i / 5); - switch (i % 5) { + var floor = Math.floor(i / 3); + switch (i % 3) { case 0: // x coordinate // Same logic as in the circular bar coords test -- - // which 5-tuplet of (x, y, r, g, b) is this + // which 3-tuplet of (x, y, rgb) is this // x-coordinate present in? This will determine what it - // SHOULD be (i.e. if this 5-tuplet is supposed to be + // SHOULD be (i.e. if this 3-tuplet is supposed to be // the "top left" of the rectangle being drawn, then // its x-coordinate should be the left x-coordinate) switch (floor) { @@ -1351,16 +1289,8 @@ require([ } break; case 2: - // Red (constant) - equal(v, 0.3); - break; - case 3: - // Green (constant) - equal(v, 0.8); - break; - case 4: - // Blue (constant) - equal(v, 0.9); + // color (constant) + equal(v, 255); break; default: throw new Error( diff --git a/tests/test-legend.js b/tests/test-legend.js index 62b643ffe..41fda658d 100644 --- a/tests/test-legend.js +++ b/tests/test-legend.js @@ -102,6 +102,10 @@ require(["jquery", "chroma", "UtilitiesForTesting", "Legend"], function ( // "rgb(255, 0, 0)". Fortunately, chroma.js recognizes this, so // we can just chuck both colors to compare through chroma // before checking equality. + // Note: firefox breaks on this test. chroma.js does not + // recognize "rgb(255, 0, 0" on firefox. This is not an + // issue for empress as we would not pass "rgb(r, g, b)" to + // chroma.js var shownColor = $(cellsInRow[0]).css("background"); // key -> color mappings should be sorted based on the key // using util.naturalSort(). So we can assume that Thing 1 is diff --git a/tests/utilities-for-testing.js b/tests/utilities-for-testing.js index 0d204e99f..5b89a36d9 100644 --- a/tests/utilities-for-testing.js +++ b/tests/utilities-for-testing.js @@ -38,13 +38,13 @@ define(["Empress", "BPTree", "BiomTable"], function ( var treeData = [ 0, // this is blank since empress uses 1-based index. This // will be addressed with #223 - [[0.75, 0.75, 0.75], false, true, 29, 30, 1, 2, 15, 16, 0, 0, 0], - [[0.75, 0.75, 0.75], false, true, 31, 32, 3, 4, 17, 18, 0, 0, 0.5], - [[0.75, 0.75, 0.75], false, true, 33, 34, 5, 6, 19, 20, 0, 0, 1], - [[0.75, 0.75, 0.75], false, true, 35, 36, 7, 8, 21, 22, 0, 0, 0], - [[0.75, 0.75, 0.75], false, true, 37, 38, 9, 10, 23, 24, 0, 0, 0], - [[0.75, 0.75, 0.75], false, true, 39, 40, 11, 12, 25, 26, 0, 0, 0], - [[0.75, 0.75, 0.75], false, true, 41, 42, 13, 14, 27, 28, 0, 0, 0], + [3289650, false, true, 29, 30, 1, 2, 15, 16, 0, 0, 0], + [3289650, false, true, 31, 32, 3, 4, 17, 18, 0, 0, 0.5], + [3289650, false, true, 33, 34, 5, 6, 19, 20, 0, 0, 1], + [3289650, false, true, 35, 36, 7, 8, 21, 22, 0, 0, 0], + [3289650, false, true, 37, 38, 9, 10, 23, 24, 0, 0, 0], + [3289650, false, true, 39, 40, 11, 12, 25, 26, 0, 0, 0], + [3289650, false, true, 41, 42, 13, 14, 27, 28, 0, 0, 0], ]; var tdToInd = { // all nodes