terriajs
Version:
Geospatial data visualization platform.
194 lines (172 loc) • 8.85 kB
JavaScript
;
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;