UNPKG

terriajs

Version:

Geospatial data visualization platform.

279 lines (251 loc) 9.15 kB
"use strict"; import { nest as d3Nest } from "d3-collection"; import { select as d3Select, event as d3Event, clientPoint as d3ClientPoint } from "d3-selection"; import { transition as d3Transition } from "d3-transition"; // eslint-disable-line no-unused-vars import defaultValue from "terriajs-cesium/Source/Core/defaultValue"; import defined from "terriajs-cesium/Source/Core/defined"; import dateformat from "dateformat"; const defaultTooltipOffset = { // The meaning of these offsets depend on the alignment. top: 10, right: 10, bottom: 10, left: 10 }; const defaultClassName = "base-chart-tooltip"; const defaultId = "base-chart-tooltip-id"; const showHideDuration = 250; /** * Handles the drawing of the chart tooltip, which shows the values of the selected data in a legend. * * @param {String} [tooltipSettings.id] The id to use for the tooltip DOM element, defaults to 'base-chart-tooltip-id'. Do not change this after creation. * @param {String} [tooltipSettings.className] The className to use for the tooltip DOM element, defaults to 'base-chart-tooltip'. Do not change this after creation. * @param {String} [tooltipSettings.align] One of 'hover' (hover at the mouse position), 'left', 'right', 'prefer-right' (chooses left or right depending on mouse position). * @param {Object} [tooltipSettings.offset] An object with top, left and right properties; these properties' meanings depend on the alignment above. * With right/left alignment, the offset is relative to the svg. */ const Tooltip = { defaultClassName: defaultClassName, defaultId: defaultId, id(tooltipSettings) { return defaultValue(tooltipSettings.id, defaultId); }, select(tooltipSettings) { return d3Select("#" + Tooltip.id(tooltipSettings)); }, create(container, tooltipSettings) { // Make the tooltip DOM element, invisible to start. if (defined(tooltipSettings)) { container .append("div") .attr("id", Tooltip.id(tooltipSettings)) .attr( "class", defaultValue(tooltipSettings.className, defaultClassName) ) .style("opacity", 1e-6) .style("position", "absolute") .style("display", "none"); } }, destroy(tooltipSettings) { // Remove the tooltip DOM element. if (defined(tooltipSettings)) { const id = Tooltip.id(tooltipSettings); const tooltipElement = d3Select("#" + id).nodes(); if (tooltipElement) { d3Select("#" + id).remove(); //NOTE: why not remove it directly like above? // tooltipElement.parentElement.removeChild(tooltipElement); } } }, singleRowHtml(color, name, value, units) { if (value === null) return; const styleAttribute = defined(color) ? 'style="background-color: ' + color + '" ' : ""; const formattedVal = isNaN(value) ? value : value.toFixed(2); return `<tr class="dataRow"> <td class="dataIcon"> <span class="color" ${styleAttribute}></span> </td> <td class="dataVal"> <span class="name">${name}</span> </td> <td class="value"> ${formattedVal} <span class="units">${units || ""}</span> </td> </tr>`; }, html(selectedData, xLocation) { let html; const readableX = typeof xLocation.getMonth === "function" ? dateformat(xLocation, "dd/mm/yyyy, HH:MMTT") : xLocation; html = '<p class="x-value">' + readableX + "</p>"; // If there is only one line showing, then label it with the category name, not the column name. // Else, if there is only one column name (shared by all the categories), show the category names // and don't show the column name. // Else, if there is only one category name, then there is no need to show it. // In general, show both, grouped by category name. // If there is only a moment dataset it's x values (a date will be shown) if ( selectedData.length === 1 && (selectedData[0].type === "moment" || selectedData[0].type === "momentPoints") ) { return html; } else if (selectedData.length === 1) { const onlyLine = selectedData[0]; html += '<p class="category-name">' + onlyLine.categoryName + "</p>"; html += "<tbody><table>"; html += this.singleRowHtml( onlyLine.color, `${onlyLine.name}`, onlyLine.type === "moment" || onlyLine.type === "momentPoints" ? readableX : onlyLine.point.y, onlyLine.units ); html += "</tbody></table>"; return html; } // The next line turns [chartData1A, chartData2, chartData1B] into // [{key: 'categoryName1', values: [chartData1A, chartData1B]}, {key: 'categoryName2', values: [chartData2]}]. const dataGroupedByCategory = d3Nest() .key((d) => d.categoryName) .entries(selectedData); // And similarly for the column names. // const dataGroupedByName = d3Nest() // .key(d => d.name) // .entries(selectedData); // if (dataGroupedByName.length === 1) { // // All lines have the same name. // html += '<table class="mouseover"><tbody>'; // dataGroupedByName[0].values.forEach(line => { // html += this.singleRowHtml( // line.color, // line.categoryName, // line.point.y, // line.units // ); // }); // html += "</tbody></table>"; // return html; // } dataGroupedByCategory.forEach((group) => { if ( group.values[0].type === "moment" || group.values[0].type === "momentPoints" ) { return; } // if (dataGroupedByCategory.length > 1) { // html += '<p class="category-name">' + group.key + "</p>"; // } html += '<p class="category-name">' + group.key + "</p>"; html += '<table class="mouseover categoryTable"><tbody>'; group.values.forEach((line) => { html += this.singleRowHtml( line.color, line.name, line.point.y, line.units ); }); html += "</tbody></table>"; }); return html; }, show(html, tooltipElement, tooltipSettings, boundingRect) { tooltipElement .html(html) .style("display", "block") .transition() .duration(showHideDuration) .style("opacity", 1) .style("max-width", "300px") .style("visibility", "visible"); const tooltipWidth = +tooltipElement.nodes()[0].offsetWidth; const tooltipOffset = defaultValue( tooltipSettings.offset, defaultTooltipOffset ); let top, left, right; const clientPos = d3ClientPoint(tooltipElement.node().parentNode, d3Event); const clientX = clientPos[0]; const clientY = clientPos[1]; switch (tooltipSettings.align) { case "left": top = tooltipOffset.top; left = tooltipOffset.left; break; case "right": top = tooltipOffset.top; right = tooltipOffset.right; break; case "prefer-right": { // Only show on the left if we would be under the tooltip on the right, but not on the left. top = tooltipOffset.top; const leftEdgeWhenPositionedRight = boundingRect.width - tooltipOffset.right - tooltipWidth; const rightEdgeWhenPositionedLeft = tooltipOffset.left + tooltipWidth; if ( clientX >= leftEdgeWhenPositionedRight && clientX > rightEdgeWhenPositionedLeft ) { left = tooltipOffset.left; } else { right = tooltipOffset.right; } break; } case "hover": default: top = d3Event.clientY - tooltipOffset.top; left = d3Event.clientX + (-tooltipWidth - tooltipOffset.left); break; } const tooltipHeight = tooltipElement.node().offsetHeight; const possibleYClash = clientY < tooltipHeight + tooltipOffset.top; if (possibleYClash) { tooltipElement.style("bottom", "60px"); tooltipElement.style("top", null); } else { tooltipElement.style("top", top + "px"); tooltipElement.style("bottom", null); } if (left !== undefined) { tooltipElement.style("left", left + "px"); } else { tooltipElement.style("left", "auto"); } if (right !== undefined) { tooltipElement.style("right", right + "px"); } else { tooltipElement.style("right", "auto"); } }, hide(tooltipElement) { tooltipElement .transition() .duration(showHideDuration) .style("opacity", 1e-6); // visibility hidden cannot transition, and it is too flashy if you use it without. // We need it because opacity=0 along can get in front of other elements and prevent the hover from working at all. // So delay it until (and only if) the opacity has already done its job. setTimeout(function () { if (+tooltipElement.style("opacity") < 0.002) { tooltipElement.style("visibility", "hidden"); } }, showHideDuration * 1.2); } }; export default Tooltip;