diff --git a/src/js/views/maps/LegendView.js b/src/js/views/maps/LegendView.js index 24a8f117e..31036bf4b 100644 --- a/src/js/views/maps/LegendView.js +++ b/src/js/views/maps/LegendView.js @@ -71,7 +71,6 @@ define([ * dimensions are set with a viewBox property only, so the height and width * represent an aspect ratio rather than absolute size. * @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 @@ -108,11 +107,15 @@ define([ * @param {object} [options] - A literal object with options to pass to the view */ 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; - }); + 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}`); } }, @@ -121,60 +124,70 @@ define([ * @returns {LegendView} Returns the rendered view element */ render() { - if (!this.model) { - return this; - } + try { + if (!this.model) { + return; + } - // The color palette maps colors to attributes of the map asset - let colorPalette = null; - // For color palettes, - let paletteType = null; - const { mode } = this; + // Save a reference to this view + const view = this; - // Insert the template into the view - this.$el.html(this.template({})); + // The color palette maps colors to attributes of the map asset + let colorPalette = null; + // For color palettes, + let paletteType = null; + const { mode } = this; - // Ensure the view's main element has the given class name - this.el.classList.add(this.className); + // Insert the template into the view + this.$el.html(this.template({})); - // Add a modifier class if this is a preview of a legend - if (mode === "preview") { - this.el.classList.add(this.classes.preview); - } + // Ensure the view's main element has the given class name + this.el.classList.add(this.className); - // 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. - Object.values(this.model.attributes).forEach((attr) => { - if (attr instanceof AssetColorPalette) { - colorPalette = attr; - paletteType = attr.get("paletteType"); + // Add a modifier class if this is a preview of a legend + if (mode === "preview") { + this.el.classList.add(this.classes.preview); } - }); - - 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); + + // 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"); + } } - // 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", () => { + + 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 { this.renderImagePreviewLegend(this.model.get("thumbnail")); - }); - } else { - this.renderImagePreviewLegend(this.model.get("thumbnail")); + } } } - } - // TODO: - // - preview classified legend - // - full legends with labels, title, etc. + // TODO: + // - preview classified legend + // - full legends with labels, title, etc. - return this; + return this; + } catch (error) { + console.log( + `There was an error rendering a Legend View` + + `. Error details: ${error}`, + ); + } }, /** @@ -183,10 +196,17 @@ define([ * image */ renderImagePreviewLegend(thumbnailURL) { - const img = new Image(); - img.src = thumbnailURL; - img.classList.add(this.classes.previewImg); - this.el.append(img); + 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}`, + ); + } }, /** @@ -196,132 +216,138 @@ define([ * feature attributes to colors, used to create the legend */ renderCategoricalPreviewLegend(colorPalette) { - if (!colorPalette) { - return; - } - const view = this; - // Data to use in d3 - let data = colorPalette.get("colors").toJSON().reverse(); + 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; - // 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); - } + 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) => { + d.i = i; + }); - // 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")) { + // 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 i + * @param hovered + */ + 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", (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(250) + .attr("x", (d, i) => getSquareX(i, true)); + } + }) + .on("mouseleave", () => { legendSquares .transition() - .duration(250) - .attr("x", (d, i) => getSquareX(i, true)); - } - }) - .on("mouseleave", () => { - legendSquares - .transition() - .duration(200) - .attr("x", (d, i) => getSquareX(i, false)); - }); + .duration(200) + .attr("x", (d, i) => getSquareX(i, false)); + }); - 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: `