From f6f8158588ea140d145dc3574e177aaab5645fb8 Mon Sep 17 00:00:00 2001 From: yvonnesjy Date: Mon, 10 Jun 2024 12:40:49 -0700 Subject: [PATCH] Fixup: Fix format and lint errors --- src/js/common/Utilities.js | 99 ++--- src/js/views/maps/LegendView.js | 748 +++++++++++++++----------------- 2 files changed, 394 insertions(+), 453 deletions(-) diff --git a/src/js/common/Utilities.js b/src/js/common/Utilities.js index c429501771..c6bbbafc2d 100644 --- a/src/js/common/Utilities.js +++ b/src/js/common/Utilities.js @@ -1,4 +1,4 @@ -define(["jquery", "underscore"], function ($, _) { +define([], () => { "use strict"; /** @@ -8,65 +8,58 @@ define(["jquery", "underscore"], function ($, _) { * @type {object} * @since 2.14.0 */ - var Utilities = /** @lends Utilities.prototype */ { + const Utilities = /** @lends Utilities.prototype */ { /** * HTML-encodes the given string so it can be inserted into an HTML page without running * any embedded Javascript. - * @param {string} s - * @returns {string} + * @param {string} s String to be encoded. + * @returns {string} HTML encoded string. */ - encodeHTML: function (s) { - try { - if (!s || typeof s !== "string") { - return ""; - } - - return s - .replace(/&/g, "&") - .replace(//g, ">") - .replace(/'/g, "'") - .replace(/\//g, "/") - .replace(/"/g, """); - } catch (e) { - console.error("Could not encode HTML: ", e); + encodeHTML(s) { + if (!s || typeof s !== "string") { return ""; } + + return s + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/'/g, "'") + .replace(/\//g, "/") + .replace(/"/g, """); }, /** * Validates that the given string is a valid DOI - * @param {string} identifier - * @returns {boolean} + * @param {string} identifier String to be validated. + * @returns {boolean} True if identifier is a valid DOI. * @since 2.15.0 */ - isValidDOI: function (identifier) { + isValidDOI(identifier) { // generate doi regex - var doiRGEX = new RegExp( - /^\s*(http:\/\/|https:\/\/)?(doi.org\/|dx.doi.org\/)?(doi: ?|DOI: ?)?(10\.\d{4,}(\.\d)*)\/(\w+).*$/gi, - ); + const doiRGEX = + /^\s*(http:\/\/|https:\/\/)?(doi.org\/|dx.doi.org\/)?(doi: ?|DOI: ?)?(10\.\d{4,}(\.\d)*)\/(\w+).*$/gi; return doiRGEX.test(identifier); }, /** * Read the first part of a file - * * @param {File} file - A reference to a file * @param {Backbone.View} context - The View to bind `callback` to - * @param {function} callback - A function to run after the read is + * @param {Function} callback - A function to run after the read is * complete. The function is bound to `context`. * @param {number} bytes - The number of bytes to read from the start of the * file * @since 2.15.0 */ - readSlice: function (file, context, callback, bytes = 1024) { + readSlice(file, context, callback, bytes = 1024) { if (typeof callback !== "function") { return; } - var reader = new FileReader(), - blob = file.slice(0, bytes); + const reader = new FileReader(); + const blob = file.slice(0, bytes); reader.onloadend = callback.bind(context); reader.readAsBinaryString(blob); @@ -77,19 +70,18 @@ define(["jquery", "underscore"], function ($, _) { * Doesn't handle: * - UTF BOM (garbles first col name) * - Commas inside quoted headers - * * @param {string} text - A chunk of a file - * @return {Array} A list of names + * @returns {Array} A list of names * @since 2.15.0 */ - tryParseCSVHeader: function (text) { + tryParseCSVHeader(text) { // The order is important here - var strategies = ["\r\\n", "\r", "\n"]; + const strategies = ["\r\\n", "\r", "\n"]; - var index = -1; + let index = -1; - for (var i = 1; i < strategies.length; i++) { - var result = text.indexOf(strategies[i]); + for (let i = 1; i < strategies.length; i += 1) { + const result = text.indexOf(strategies[i]); if (result >= 0) { index = result; @@ -102,18 +94,14 @@ define(["jquery", "underscore"], function ($, _) { return []; } - var header_line = text.slice(0, index); - var names = header_line.split(","); + const headerLine = text.slice(0, index); + let names = headerLine.split(","); // Remove surrounding parens and double-quotes - names = names.map(function (name) { - return name.replaceAll(/^["']|["']$/gm, ""); - }); + names = names.map((name) => name.replaceAll(/^["']|["']$/gm, "")); // Filter out zero-length values (headers like a,b,c,,,,,) - names = names.filter(function (name) { - return name.length > 0; - }); + names = names.filter((name) => name.length > 0); return names; }, @@ -126,7 +114,9 @@ define(["jquery", "underscore"], function ($, _) { const roundingConstant = Utilities.getRoundingConstant(max - min); if (roundingConstant) { - return (Math.round(value * roundingConstant) / roundingConstant).toString(); + return ( + Math.round(value * roundingConstant) / roundingConstant + ).toString(); } return value.toExponential(2).toString(); }, @@ -136,19 +126,24 @@ define(["jquery", "underscore"], function ($, _) { getRoundingConstant(range) { if (range < 0.0001 || range > 100000) { return null; // Will use scientific notation - } else if (range < 0.001) { + } + if (range < 0.001) { return 100000; // Allow 5 decimal places - } else if (range < 0.01) { + } + if (range < 0.01) { return 10000; // Allow 4 decimal places - } else if (range < 0.1) { + } + if (range < 0.1) { return 1000; // Allow 3 decimal places - } else if (range < 1) { + } + if (range < 1) { return 100; // Allow 2 decimal places - } else if (range > 100) { + } + if (range > 100) { return 1; // No decimal places } return 10; // Allow 1 decimal place by default - } + }, }; return Utilities; diff --git a/src/js/views/maps/LegendView.js b/src/js/views/maps/LegendView.js index 26c962fcbe..a52b9e742d 100644 --- a/src/js/views/maps/LegendView.js +++ b/src/js/views/maps/LegendView.js @@ -8,7 +8,7 @@ define([ "models/maps/AssetColorPalette", "common/Utilities", "text!templates/maps/legend.html", -], function ($, _, Backbone, d3, AssetColorPalette, Utilities, Template) { +], ($, _, Backbone, d3, AssetColorPalette, Utilities, Template) => { /** * @class LegendView * @classdesc Creates a legend for a given Map Asset (Work In Progress). Currently @@ -18,12 +18,12 @@ define([ * palettes (including 'continuous' and 'classified') * @classcategory Views/Maps * @name LegendView - * @extends Backbone.View + * @augments Backbone.View * @screenshot views/maps/LegendView.png * @since 2.18.0 * @constructs */ - var LegendView = Backbone.View.extend( + const LegendView = Backbone.View.extend( /** @lends LegendView.prototype */ { /** * The type of View this is @@ -52,7 +52,7 @@ define([ /** * The events this view will listen to and the associated function to call. - * @type {Object} + * @type {object} */ events: { // 'event selector': 'function', @@ -70,7 +70,8 @@ define([ * For vector preview legends, the relative dimensions to use. The SVG's * dimensions are set with a viewBox property only, so the height and width * represent an aspect ratio rather than absolute size. - * @type {Object} + * @type {object} + * @property {object} previewSvgDimensions - The dimension properties of the SVG. * @property {number} previewSvgDimensions.width - The width of the entire SVG * @property {number} previewSvgDimensions.height - The height of the entire SVG * @property {number} squareSpacing - Maximum spacing between each of the squares @@ -86,7 +87,7 @@ define([ /** * Classes that are used to identify, or that are added to, the HTML elements that * comprise this view. - * @type {Object} + * @type {object} * @property {string} preview Additional class to add to legend that are the * preview/thumbnail version * @property {string} previewSVG The SVG element that holds the shapes with all @@ -104,91 +105,76 @@ define([ /** * Executed when a new LegendView is created - * @param {Object} [options] - A literal object with options to pass to the view + * @param {object} [options] - A literal object with options to pass to the view */ - initialize: function (options) { - try { - // Get all the options and apply them to this view - if (typeof options == "object") { - for (const [key, value] of Object.entries(options)) { - this[key] = value; - } - } - } catch (e) { - console.log("A LegendView failed to initialize. Error message: " + e); + initialize(options) { + // Get all the options and apply them to this view + if (typeof options === "object") { + Object.entries(options).forEach(([key, value]) => { + this[key] = value; + }); } }, /** * Renders this view - * @return {LegendView} Returns the rendered view element + * @returns {LegendView} Returns the rendered view element */ - render: function () { - try { - if (!this.model) { - return; - } + render() { + if (!this.model) { + return this; + } - // Save a reference to this view - var view = this; + // The color palette maps colors to attributes of the map asset + let colorPalette = null; + // For color palettes, + let paletteType = null; + const { mode } = this; - // The color palette maps colors to attributes of the map asset - let colorPalette = null; - // For color palettes, - let paletteType = null; - const mode = this.mode; + // Insert the template into the view + this.$el.html(this.template({})); - // Insert the template into the view - this.$el.html(this.template({})); + // Ensure the view's main element has the given class name + this.el.classList.add(this.className); - // Ensure the view's main element has the given class name - this.el.classList.add(this.className); + // Add a modifier class if this is a preview of a legend + if (mode === "preview") { + this.el.classList.add(this.classes.preview); + } - // Add a modifier class if this is a preview of a legend - if (mode === "preview") { - this.el.classList.add(this.classes.preview); + // Check for a color palette model in the Map Asset model. Even imagery layers + // may have a color palette configured, specifically to use to create a + // legend. + this.model.attributes.keys().forEach((attr) => { + if (this.model.attributes[attr] instanceof AssetColorPalette) { + colorPalette = this.model.get(attr); + paletteType = colorPalette.get("paletteType"); } - - // Check for a color palette model in the Map Asset model. Even imagery layers - // may have a color palette configured, specifically to use to create a - // legend. - for (const attr in this.model.attributes) { - if (this.model.attributes[attr] instanceof AssetColorPalette) { - colorPalette = this.model.get(attr); - paletteType = colorPalette.get("paletteType"); - } + }); + + if (mode === "preview") { + // For categorical vector color palettes, in preview mode + if (colorPalette && paletteType === "categorical") { + this.renderCategoricalPreviewLegend(colorPalette); + } else if (colorPalette && paletteType === "continuous") { + this.renderContinuousPreviewLegend(colorPalette); } - - if (mode === "preview") { - // For categorical vector color palettes, in preview mode - if (colorPalette && paletteType === "categorical") { - this.renderCategoricalPreviewLegend(colorPalette); - } else if (colorPalette && paletteType === "continuous") { - this.renderContinuousPreviewLegend(colorPalette); - } - // For imagery layers that do not have a color palette, in preview mode - else if (typeof this.model.getThumbnail === "function") { - if (!this.model.get("thumbnail")) { - this.listenToOnce(this.model, "change:thumbnail", function () { - this.renderImagePreviewLegend(this.model.get("thumbnail")); - }); - } else { + // For imagery layers that do not have a color palette, in preview mode + else if (typeof this.model.getThumbnail === "function") { + if (!this.model.get("thumbnail")) { + this.listenToOnce(this.model, "change:thumbnail", () => { this.renderImagePreviewLegend(this.model.get("thumbnail")); - } + }); + } else { + this.renderImagePreviewLegend(this.model.get("thumbnail")); } } - // TODO: - // - preview classified legend - // - full legends with labels, title, etc. - - return this; - } catch (error) { - console.log( - "There was an error rendering a Legend View" + - ". Error details: " + - error, - ); } + // TODO: + // - preview classified legend + // - full legends with labels, title, etc. + + return this; }, /** @@ -196,19 +182,11 @@ define([ * @param {string} thumbnailURL A url to use for the src property of the thumbnail * image */ - renderImagePreviewLegend: function (thumbnailURL) { - try { - const img = new Image(); - img.src = thumbnailURL; - img.classList.add(this.classes.previewImg); - this.el.append(img); - } catch (error) { - console.log( - "There was an error rendering an image preview legend in a LegendView" + - ". Error details: " + - error, - ); - } + renderImagePreviewLegend(thumbnailURL) { + const img = new Image(); + img.src = thumbnailURL; + img.classList.add(this.classes.previewImg); + this.el.append(img); }, /** @@ -217,142 +195,133 @@ define([ * @param {AssetColorPalette} colorPalette - The AssetColorPalette that maps * feature attributes to colors, used to create the legend */ - renderCategoricalPreviewLegend: function (colorPalette) { - try { - if (!colorPalette) { - return; - } - const view = this; - // Data to use in d3 - let data = colorPalette.get("colors").toJSON().reverse(); - - if (data.length === 0) { - return; - } - // The max width of the SVG, to be reduced if there are few colours - let width = this.previewSvgDimensions.width; - // The height of the SVG - const height = this.previewSvgDimensions.height; - // Height and width of the square is the height of the SVG, leaving some room - // for shadow to show - const squareSize = height * 0.92; - // Maximum spacing between squares. When not hovered, the squares will be - // spaced 80% of this value. - let squareSpacing = this.previewSvgDimensions.squareSpacing; - // The maximum number of squares that can fit on the SVG without any spilling - // over - const maxNumSquares = Math.floor( - (width - squareSize) / squareSpacing + 1, - ); - - // If there are more colors than fit in the max width of the SVG space, only - // show the first n squares that will fit - if (data.length > maxNumSquares) { - data = data.slice(0, maxNumSquares); - } - // Add index to data for sorting later (also works as unique ID) - data.forEach(function (d, i) { - d.i = i; - }); - - // Don't create an SVG that is wider than it need to be. - width = squareSize + (data.length - 1) * squareSpacing; - - // SVG element - const svg = this.createSVG({ - dropshadowFilter: true, - width: width, - height: height, - }); - - // Add the preview class and dropshadow to the SVG - svg.classed(this.classes.previewSVG, true); - svg.style("filter", "url(#dropshadow)"); + renderCategoricalPreviewLegend(colorPalette) { + if (!colorPalette) { + return; + } + const view = this; + // Data to use in d3 + let data = colorPalette.get("colors").toJSON().reverse(); - // Calculates the placement of the square along x-axis, when SVG is hovered - // and when it's not - function getSquareX(i, hovered) { - const multiplier = hovered ? 1 : 0.8; - return width - squareSize - i * (squareSpacing * multiplier); - } + if (data.length === 0) { + return; + } + // The max width of the SVG, to be reduced if there are few colours + let { width } = this.previewSvgDimensions; + // The height of the SVG + const { height } = this.previewSvgDimensions; + // Height and width of the square is the height of the SVG, leaving some room + // for shadow to show + const squareSize = height * 0.92; + // Maximum spacing between squares. When not hovered, the squares will be + // spaced 80% of this value. + const { squareSpacing } = this.previewSvgDimensions; + // The maximum number of squares that can fit on the SVG without any spilling + // over + const maxNumSquares = Math.floor( + (width - squareSize) / squareSpacing + 1, + ); + + // If there are more colors than fit in the max width of the SVG space, only + // show the first n squares that will fit + if (data.length > maxNumSquares) { + data = data.slice(0, maxNumSquares); + } + // Add index to data for sorting later (also works as unique ID) + data.forEach((d, i) => { + // eslint-disable-next-line no-param-reassign + d.i = i; + }); + + // Don't create an SVG that is wider than it need to be. + width = squareSize + (data.length - 1) * squareSpacing; + + // SVG element + const svg = this.createSVG({ + dropshadowFilter: true, + width, + height, + }); + + // Add the preview class and dropshadow to the SVG + svg.classed(this.classes.previewSVG, true); + svg.style("filter", "url(#dropshadow)"); + + /** + * Calculates the placement of the square along x-axis, when SVG is hovered + * and when it's not + * @param {number} i Index of the data. + * @param {boolean} hovered Whether the SVG is on hover. + * @returns {number} The placement of the square along x-axis in pixel. + */ + function getSquareX(i, hovered) { + const multiplier = hovered ? 1 : 0.8; + return width - squareSize - i * (squareSpacing * multiplier); + } - // Draw the legend (d3) - const legendSquares = svg - .selectAll("rect") - .data(data) - .enter() - .append("rect") - .attr("x", function (d, i) { - return getSquareX(i, false); - }) - .attr("height", squareSize) - .attr("width", squareSize) - .attr("rx", squareSize * 0.1) - .style("fill", function (d) { - return `rgb(${d.color.red * 255},${d.color.green * 255},${d.color.blue * 255})`; - }) - .style("filter", "url(#dropshadow)"); - - // For legend with multiple colours, show a tooltip with the value/label when - // the user hovers over a square. Also bring that square to the fore-front of - // the legend when hovered. Only when MapAsset is visible though. - if (data.length > 1) { - // Space the squares further apart when they are hovered over - svg - .on("mouseenter", function () { - if (view.model.get("visible")) { - legendSquares - .transition() - .duration(250) - .attr("x", function (d, i) { - return getSquareX(i, true); - }); - } - }) - .on("mouseleave", function () { + // Draw the legend (d3) + const legendSquares = svg + .selectAll("rect") + .data(data) + .enter() + .append("rect") + .attr("x", (d, i) => getSquareX(i, false)) + .attr("height", squareSize) + .attr("width", squareSize) + .attr("rx", squareSize * 0.1) + .style( + "fill", + (d) => + `rgb(${d.color.red * 255},${d.color.green * 255},${d.color.blue * 255})`, + ) + .style("filter", "url(#dropshadow)"); + + // For legend with multiple colours, show a tooltip with the value/label when + // the user hovers over a square. Also bring that square to the fore-front of + // the legend when hovered. Only when MapAsset is visible though. + if (data.length > 1) { + // Space the squares further apart when they are hovered over + svg + .on("mouseenter", () => { + if (view.model.get("visible")) { legendSquares .transition() - .duration(200) - .attr("x", function (d, i) { - return getSquareX(i, false); - }); - }); + .duration(250) + .attr("x", (d, i) => getSquareX(i, true)); + } + }) + .on("mouseleave", () => { + legendSquares + .transition() + .duration(200) + .attr("x", (d, i) => getSquareX(i, false)); + }); - legendSquares - .on("mouseenter", function (d) { - // Bring the hovered element to the front, while keeping other - // legendSquares in order - legendSquares.sort((a, b) => d3.ascending(a.i, b.i)); - this.parentNode.appendChild(this); - // Show tooltip - if (d.label || d.value || d.value === 0) { - $(this) - .tooltip({ - placement: "bottom", - trigger: "manual", - title: d.label || d.value, - container: view.$el, - animation: false, - template: - '
', - }) - .tooltip("show"); - } - }) - // Hide tooltip and return squares to regular z-ordering - .on("mouseleave", function (d) { - $(this).tooltip("destroy"); - legendSquares.sort((a, b) => d3.ascending(a.i, b.i)); - }); - } - } catch (error) { - console.log( - "There was an error creating a categorical legend preview in a LegendView" + - ". Error details: " + - error, - ); + legendSquares + .on("mouseenter", (d) => { + // Bring the hovered element to the front, while keeping other + // legendSquares in order + legendSquares.sort((a, b) => d3.ascending(a.i, b.i)); + this.parentNode.appendChild(this); + // Show tooltip + if (d.label || d.value || d.value === 0) { + $(this) + .tooltip({ + placement: "bottom", + trigger: "manual", + title: d.label || d.value, + container: view.$el, + animation: false, + template: `
`, + }) + .tooltip("show"); + } + }) + // Hide tooltip and return squares to regular z-ordering + .on("mouseleave", () => { + $(this).tooltip("destroy"); + legendSquares.sort((a, b) => d3.ascending(a.i, b.i)); + }); } }, @@ -362,205 +331,182 @@ define([ * @param {AssetColorPalette} colorPalette - The AssetColorPalette that maps * feature attributes to colors, used to create the legend */ - renderContinuousPreviewLegend: function (colorPalette) { - try { - if (!colorPalette) { - return; - } - const view = this; - // Data to use in d3 - let data = colorPalette.get("colors").toJSON(); - // The max width of the SVG - let width = this.previewSvgDimensions.width; - // The height of the SVG - const height = this.previewSvgDimensions.height; - // Height of the gradient rectangle, leaving some room for the drop shadow - const gradientHeight = height * 0.92; - - // A unique ID for the gradient - const gradientId = "gradient-" + view.cid; - - // Calculate the rounding precision we should use based on the - // range of the data. This determines how each value in the legend - // is displayed in the tooltip on mouseover. See the - // rect.on('mousemove'... function, below - data = data.sort((a, b) => a.value - b.value); - const min = data[0].value; - const max = data[data.length - 1].value; - const range = max - min; - const roundingConstant = Utilities.getRoundingConstant(range); - - // SVG element - const svg = this.createSVG({ - dropshadowFilter: false, - width: width, - height: height, - }); - - // Add the preview class and dropshadow to the SVG - svg.classed(this.classes.previewSVG, true); - svg.style("filter", "url(#dropshadow)"); - - // Create a gradient using the data - const gradient = svg - .append("defs") - .append("linearGradient") - .attr("id", gradientId) - .attr("x1", "0%") - .attr("y1", "0%"); - - var getOffset = function (d, data) { - return ((d.value - min) / range) * 100 + "%"; - }; - var getStopColor = function (d) { - const r = d.color.red * 255; - const g = d.color.green * 255; - const b = d.color.blue * 255; - return `rgb(${r},${g},${b})`; - }; - - // Add the gradient stops - data.forEach(function (d, i) { - gradient - .append("stop") - // offset should be relative to the value in the data - .attr("offset", getOffset(d, data)) - .attr("stop-color", getStopColor(d)); - }); - - // Create the rectangle - const rect = svg - .append("rect") - .attr("x", 0) - .attr("y", 0) - .attr("width", width) - .attr("height", gradientHeight) - .attr("rx", gradientHeight * 0.1) - .style("fill", "url(#" + gradientId + ")"); - - // Create a proxy element to attach the tooltip to, so that we can move the - // tooltip to follow the mouse (by moving the proxy element to follow the mouse) - const proxyEl = svg.append("rect").attr("y", gradientHeight); - - rect - .on("mousemove", function () { - if (view.model.get("visible")) { - // Get the coordinates of the mouse relative to the rectangle - let xMouse = d3.mouse(this)[0]; - if (xMouse < 0) { - xMouse = 0; - } - if (xMouse > width) { - xMouse = width; - } - // Get the relative position of the mouse to the gradient - const relativePosition = xMouse / width; - // Get the value at the relative position by interpolating the data - let value = d3.interpolate( - data[0].value, - data[data.length - 1].value, - )(relativePosition); - // Show tooltip with the value - if (value || value === 0) { - // Round or show in scientific notation - if (roundingConstant) { - value = ( - Math.round(value * roundingConstant) / roundingConstant - ).toString(); - } else { - value = value.toExponential(2).toString(); - } - // Move the proxy element to follow the mouse - proxyEl.attr("x", xMouse); - // Attach the tooltip to the proxy element. Tooltip needs to be - // refreshed every time the mouse moves - $(proxyEl).tooltip("destroy"); - $(proxyEl) - .tooltip({ - placement: "bottom", - trigger: "manual", - title: value, - container: view.$el, - animation: false, - template: - '
', - }) - .tooltip("show"); + renderContinuousPreviewLegend(colorPalette) { + if (!colorPalette) { + return; + } + const view = this; + // Data to use in d3 + let data = colorPalette.get("colors").toJSON(); + // The max width of the SVG + const { width } = this.previewSvgDimensions; + // The height of the SVG + const { height } = this.previewSvgDimensions; + // Height of the gradient rectangle, leaving some room for the drop shadow + const gradientHeight = height * 0.92; + + // A unique ID for the gradient + const gradientId = `gradient-${view.cid}`; + + // Calculate the rounding precision we should use based on the + // range of the data. This determines how each value in the legend + // is displayed in the tooltip on mouseover. See the + // rect.on('mousemove'... function, below + data = data.sort((a, b) => a.value - b.value); + const min = data[0].value; + const max = data[data.length - 1].value; + const range = max - min; + const roundingConstant = Utilities.getRoundingConstant(range); + + // SVG element + const svg = this.createSVG({ + dropshadowFilter: false, + width, + height, + }); + + // Add the preview class and dropshadow to the SVG + svg.classed(this.classes.previewSVG, true); + svg.style("filter", "url(#dropshadow)"); + + // Create a gradient using the data + const gradient = svg + .append("defs") + .append("linearGradient") + .attr("id", gradientId) + .attr("x1", "0%") + .attr("y1", "0%"); + + const getOffset = (d) => `${((d.value - min) / range) * 100}%`; + const getStopColor = (d) => { + const r = d.color.red * 255; + const g = d.color.green * 255; + const b = d.color.blue * 255; + return `rgb(${r},${g},${b})`; + }; + + // Add the gradient stops + data.forEach((d) => { + gradient + .append("stop") + // offset should be relative to the value in the data + .attr("offset", getOffset(d, data)) + .attr("stop-color", getStopColor(d)); + }); + + // Create the rectangle + const rect = svg + .append("rect") + .attr("x", 0) + .attr("y", 0) + .attr("width", width) + .attr("height", gradientHeight) + .attr("rx", gradientHeight * 0.1) + .style("fill", `url(#${gradientId})`); + + // Create a proxy element to attach the tooltip to, so that we can move the + // tooltip to follow the mouse (by moving the proxy element to follow the mouse) + const proxyEl = svg.append("rect").attr("y", gradientHeight); + + rect + .on("mousemove", () => { + if (view.model.get("visible")) { + // Get the coordinates of the mouse relative to the rectangle + let xMouse = d3.mouse(this)[0]; + if (xMouse < 0) { + xMouse = 0; + } + if (xMouse > width) { + xMouse = width; + } + // Get the relative position of the mouse to the gradient + const relativePosition = xMouse / width; + // Get the value at the relative position by interpolating the data + let value = d3.interpolate( + data[0].value, + data[data.length - 1].value, + )(relativePosition); + // Show tooltip with the value + if (value || value === 0) { + // Round or show in scientific notation + if (roundingConstant) { + value = ( + Math.round(value * roundingConstant) / roundingConstant + ).toString(); + } else { + value = value.toExponential(2).toString(); } + // Move the proxy element to follow the mouse + proxyEl.attr("x", xMouse); + // Attach the tooltip to the proxy element. Tooltip needs to be + // refreshed every time the mouse moves + $(proxyEl).tooltip("destroy"); + $(proxyEl) + .tooltip({ + placement: "bottom", + trigger: "manual", + title: value, + container: view.$el, + animation: false, + template: `
`, + }) + .tooltip("show"); } - }) - // Hide tooltip - .on("mouseleave", function () { - $(proxyEl).tooltip("destroy"); - }); - } catch (error) { - console.log( - "There was an error rendering a continuous preview legend in a LegendView" + - ". Error details: " + - error, - ); - } + } + }) + // Hide tooltip + .on("mouseleave", () => { + $(proxyEl).tooltip("destroy"); + }); }, /** * Creates an SVG element and inserts it into the view * @param {object} options Used to configure parts of the SVG - * @property {boolean} options.dropshadowFilter Set to true to create a filter + * @property {boolean} dropshadowFilter Set to true to create a filter * element that creates a dropshadow behind any element it is applied to. It can * be added to child elements of the SVG by setting a `filter: url(#dropshadow);` * style rule on the child. - * @property {number} options.height The relative height of the SVG (for the + * @property {number} height The relative height of the SVG (for the * viewBox property) - * @property {number} options.width The relative width of the SVG (for the viewBox + * @property {number} width The relative width of the SVG (for the viewBox * property) * @returns {SVG} Returns the SVG element that is in the view */ - createSVG: function (options = {}) { - try { - // Create an SVG to hold legend elements - const container = this.el; - const width = options.width; - const height = options.height; - - const svg = d3 - .select(container) - .append("svg") - .attr("preserveAspectRatio", "xMidYMid") - .attr("viewBox", [0, 0, width, height]); - - if (options.dropshadowFilter) { - const filterText = ` - - - - - - - - - - `; - - const filterEl = new DOMParser().parseFromString( - '' + - filterText + - "", - "application/xml", - ).documentElement.firstChild; - - svg.node().appendChild(document.importNode(filterEl, true)); - } - - return svg; - } catch (error) { - console.log( - "There was an error creating an SVG in a LegendView" + - ". Error details: " + - error, - ); + createSVG(options = {}) { + // Create an SVG to hold legend elements + const container = this.el; + const { width } = options; + const { height } = options; + + const svg = d3 + .select(container) + .append("svg") + .attr("preserveAspectRatio", "xMidYMid") + .attr("viewBox", [0, 0, width, height]); + + if (options.dropshadowFilter) { + const filterText = ` + + + + + + + + + + `; + + const filterEl = new DOMParser().parseFromString( + `${filterText}`, + "application/xml", + ).documentElement.firstChild; + + svg.node().appendChild(document.importNode(filterEl, true)); } + + return svg; }, }, );