UNPKG

terriajs

Version:

Geospatial data visualization platform.

194 lines (172 loc) 8.85 kB
'use strict'; import {nest as d3Nest} from 'd3-collection'; import {select as d3Select, event as d3Event} 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'; const defaultTooltipOffset = { // The meaning of these offsets depend on the alignment. top: 10, right: 10, bottom: 10, left: 10 }; const defaultClassName = 'linechart-tooltip'; const defaultId = 'linechart-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 'linechart-tooltip-id'. Do not change this after creation. * @param {String} [tooltipSettings.className] The className to use for the tooltip DOM element, defaults to 'linechart-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(tooltipSettings) { // Make the tooltip DOM element, invisible to start. // This must go on the body to allow absolute positioning to work. if (defined(tooltipSettings)) { d3Select('body') .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) { const styleAttribute = defined(color) ? ('style="background-color: ' + color + '" ') : ''; let html = '<tr>'; html += '<td><span class="color" ' + styleAttribute + '></span><span class="name">' + name + '</span></td>'; html += '<td class="value">' + value + ' <span class="units">' + (units || '') + '</span></td>'; html += '</tr>'; return html; }, html(selectedData) { if (selectedData.length === 0) { return ''; } let html; const x = selectedData[0].point.x; // All the data has the same x. html = '<p class="x-value">' + x + '</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 (selectedData.length === 1) { html += '<table class="mouseover"><tbody>'; const onlyLine = selectedData[0]; html += this.singleRowHtml(onlyLine.color, onlyLine.categoryName, 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 (dataGroupedByCategory.length > 1) { html += '<p class="category-name">' + group.key + '</p>'; } html += '<table class="mouseover"><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('opacity', 1e-6) .style('display', 'block') .style('visibility', 'hidden') .transition().duration(showHideDuration) .style('opacity', 1) .style('visibility', 'visible'); const tooltipHeight = +tooltipElement.nodes()[0].offsetHeight; const tooltipWidth = +tooltipElement.nodes()[0].offsetWidth; const tooltipOffset = defaultValue(tooltipSettings.offset, defaultTooltipOffset); let top, left; switch (tooltipSettings.align) { case 'left': top = +boundingRect.top + tooltipOffset.top; left = +boundingRect.left + tooltipOffset.left; break; case 'right': top = +boundingRect.top + tooltipOffset.top; left = +boundingRect.right - tooltipOffset.right - tooltipWidth; 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 = +boundingRect.top + tooltipOffset.top; // console.log(boundingRect.top); // Occasionally the top jerks around a little? left = +boundingRect.right - tooltipOffset.right - tooltipWidth; const rightEdgeWhenPositionedLeft = +boundingRect.left + tooltipOffset.left + tooltipWidth; if ((d3Event.pageX >= left) && (d3Event.pageX > rightEdgeWhenPositionedLeft)) { left = +boundingRect.left + tooltipOffset.left; } break; } case 'hover': top = d3Event.pageY - tooltipOffset.top; left = d3Event.pageX + (-tooltipWidth - tooltipOffset.left); break; default: // Same as hover. Would prefer not to have the break above, but lint requires it. top = d3Event.pageY - tooltipOffset.top; left = d3Event.pageX + (-tooltipWidth - tooltipOffset.left); } // Make sure the bottom of the tooltip never goes off the bottom of the screen. top = Math.min(top, Math.max(5, window.innerHeight - tooltipHeight - tooltipOffset.bottom)); tooltipElement .style('left', left + 'px') .style('top', top + 'px'); }, 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); } }; module.exports = Tooltip;