d2b
Version:
315 lines (259 loc) • 9.86 kB
JavaScript
import { select, event as d3Event } from 'd3-selection';
import { ascending } from 'd3-array';
import 'd3-transition';
import base from '../model/base';
import oreq from '../util/oreq';
import oreqUndefined from './oreqUndefined';
export default function () {
const $$ = {};
const tooltip = {};
// Position markers relative to selected points and axes
const positionMarker = function (marker, info, type) {
if (type === 'y') {
if (info.y === Infinity) return marker.style('opacity', 0);
marker
.style('opacity', 1)
.attr('transform', `translate(0, ${info.y})`)
.attr('y1', 0).attr('y2', 0).attr('x1', 0).attr('x2', $$.size.width);
} else {
if (info.x === Infinity) return marker.style('opacity', 0);
marker
.style('opacity', 1)
.attr('transform', `translate(${info.x}, 0)`)
.attr('x1', 0).attr('x2', 0).attr('y1', 0).attr('y2', $$.size.height);
}
};
// Position tooltip relative to selected points and axes
const positionTooltip = function (tooltip, info, base) {
const node = tooltip.node();
if (!node) return;
const tooltipBox = tooltip.node().getBoundingClientRect();
let x = base.x, y = base.y, pad = 10;
if ($$.trackY) {
if (info.y > $$.size.height / 2) {
y += info.y - pad - tooltipBox.height;
} else {
y += info.y + pad;
}
} else {
if (d3Event.clientY - base.y > $$.size.height / 2) {
y = d3Event.clientY - pad - tooltipBox.height;
} else {
y = d3Event.clientY + pad;
}
}
if ($$.trackX) {
if (info.x > $$.size.width / 2) {
x += info.x - pad - tooltipBox.width;
} else {
x += info.x + pad;
}
} else {
if (d3Event.clientX - base.x > $$.size.width / 2) {
x = d3Event.clientX - pad - tooltipBox.width;
} else {
x = d3Event.clientX + pad;
}
}
x += window.pageXOffset;
y += window.pageYOffset;
tooltip.style('left', x+'px').style('top', y+'px');
};
// Populate tooltip with point rows
const populateTooltip = function (tooltip, info) {
const title = $$.title(info.points.map(d => d.data));
tooltip.select('.d2b-tooltip-title')
.style('display', title? 'block' : 'none')
.html(title);
const content = tooltip.select('.d2b-tooltip-content');
let row = content.selectAll('.d2b-tooltip-row').data(info.points);
const rowEnter = row.enter().append('div').attr('class', 'd2b-tooltip-row');
row.exit().remove();
row = row.merge(rowEnter)
.html(d => d.row)
.style('border-left-color', d => d.color || 'transparent');
};
// Finds the x, y coordinates associated with the points 'closest' to the cursor.
// Also returns the set of points that meet the 'closest' configuration.
const findPointInfo = function (base) {
const cursor = {x: d3Event.clientX - base.x, y: d3Event.clientY - base.y};
let x = Infinity, y = Infinity, points = [];
for (let groupName in groups) {
if (!groups.hasOwnProperty(groupName)) continue;
const group = groups[groupName];
for (let graphName in group) {
if(!group.hasOwnProperty(graphName)) continue;
const graph = group[graphName];
let newPoints = [];
graph.config.data.forEach((d, i) => {
const item = {
data: d,
x: oreq(graph.config.x(d, i), $$.x(d, i)),
y: oreq(graph.config.y(d, i), $$.y(d, i)),
color: oreq(graph.config.color(d, i), $$.color(d, i)),
row: oreqUndefined(graph.config.row(d, i), $$.row(d, i))
};
if ($$.trackX && $$.trackY) {
if (item.x === x && item.y === y) return newPoints.push(item);
const od = Math.sqrt(Math.pow(x - cursor.x, 2) + Math.pow(y - cursor.y, 2));
const nd = Math.sqrt(Math.pow(item.x - cursor.x, 2) + Math.pow(item.y - cursor.y, 2));
if (nd < od && nd < $$.threshold) {
x = item.x;
y = item.y;
points = [];
newPoints = [item];
}
} else if ($$.trackX) {
if (item.x === x) return newPoints.push(item);
const od = Math.abs(x - cursor.x);
const nd = Math.abs(item.x - cursor.x);
if (nd < od && nd < $$.threshold) {
x = item.x;
points = [];
newPoints = [item];
}
} else if ($$.trackY) {
if (item.y === y) return newPoints.push(item);
const od = Math.abs(y - cursor.y);
const nd = Math.abs(item.y - cursor.y);
if (nd < od && nd < $$.threshold) {
y = item.y;
points = [];
newPoints = [item];
}
}
});
points = points.concat(newPoints);
}
}
points = points.sort((a, b) => {
return ascending(a.x, b.x) || ascending(a.y, b.y);
});
return {x: x, y: y, points: points};
};
// Exit tooltip element.
const exitElement = function (el) {
el.transition().duration(50).style('opacity', 0).remove();
};
// Enter tooltip element.
const enterElement = function (el) {
el.transition().duration(50).style('opacity', 1);
};
// Enter tooltip components.
const enter = function (d, i) {
const markerX = $$.svgContainer.selectAll('.d2b-tooltip-marker-x').data($$.trackX? [tooltip] : []);
const markerXEnter = markerX.enter()
.append('line')
.attr('class', 'd2b-tooltip-marker-x d2b-tooltip-marker');
const markerY = $$.svgContainer.selectAll('.d2b-tooltip-marker-y').data($$.trackY? [tooltip] : []);
const markerYEnter = markerY.enter()
.append('line')
.attr('class', 'd2b-tooltip-marker-y d2b-tooltip-marker');
const tooltipEl = $$.htmlContainer.selectAll('.d2b-tooltip-axis').data([tooltip]);
const tooltipEnter = tooltipEl.enter()
.append('div')
.attr('class', 'd2b-tooltip d2b-tooltip-axis');
tooltipEnter.merge(tooltipEl).call(enterElement);
markerY.merge(markerYEnter).call(enterElement);
markerX.merge(markerXEnter).call(enterElement);
tooltipEnter.append('div').attr('class', 'd2b-tooltip-title');
tooltipEnter.append('div').attr('class', 'd2b-tooltip-content');
$$.dispatch.call('insert', $$.tooltip, d, i);
};
// Exit tooltip components.
const exit = function () {
$$.svgContainer.selectAll('.d2b-tooltip-marker-x').data([]).exit().call(exitElement);
$$.svgContainer.selectAll('.d2b-tooltip-marker-y').data([]).exit().call(exitElement);
$$.htmlContainer.selectAll('.d2b-tooltip-axis').data([]).exit().call(exitElement);
};
// Tracker mousemove event.
const mousemove = function (d, i) {
let base = $$.svgContainer.selectAll('.d2b-tooltip-base').data([d]);
base.enter().append('rect').attr('class', 'd2b-tooltip-base');
let baseBox = $$.svgContainer.node().getBoundingClientRect();
baseBox = {x: baseBox.left, y: baseBox.top};
const pointInfo = findPointInfo(baseBox);
if (pointInfo.points.length) enter();
else return exit();
$$.svgContainer
.select('.d2b-tooltip-marker-x')
.call(positionMarker, pointInfo, 'x');
$$.svgContainer
.select('.d2b-tooltip-marker-y')
.call(positionMarker, pointInfo, 'y');
$$.htmlContainer
.select('.d2b-tooltip-axis')
.call(populateTooltip, pointInfo)
.call(positionTooltip, pointInfo, baseBox);
$$.dispatch.call('move', $$.tooltip, d, i);
};
// Tracker mouseout event.
const mouseout = function (d, i) {
exit();
$$.dispatch.call('remove', $$.tooltip, d, i);
};
// Event key builder.
const event = (listener) => {
return `${listener}.d2b-tooltip-axis`;
};
// update mouse event tracker
const updateTracker = (n, o) => {
if (o) {
o
.on(event('mouseout'), null)
.on(event('mousemove'), null);
}
if (n) {
n
.on(event('mouseout'), mouseout)
.on(event('mousemove'), mousemove);
}
};
// setup tooltip model
base(tooltip, $$)
.addProp('htmlContainer', select('body'))
.addProp('svgContainer', null)
.addProp('tracker', select('body'), null, updateTracker)
.addProp('size', {height: 0, width: 0})
.addProp('trackX', true)
.addProp('trackY', false)
.addProp('threshold', Infinity)
.addMethod('clear', function (groupName, graphName) {
if (arguments.length === 0) groups = {};
else if (arguments.length === 1) delete groups[groupName];
else if (arguments.length >= 2) delete groups[groupName][graphName];
return tooltip;
})
.addPropFunctor('title', null)
.addPropFunctor('x', d => d.x)
.addPropFunctor('y', d => d.y)
.addPropFunctor('color', null)
.addPropFunctor('row', null)
.addDispatcher(['move', 'insert', 'remove']);
// construct an interface for each graph that is initialized
let groups = {};
tooltip.graph = function (groupName = null, graphName = null) {
const graphs = groups[groupName] = groups[groupName] || {};
let graph = graphs[graphName];
if (!graph) {
graph = graphs[graphName] = {interface: {},config: {}};
const graphModel = base(graph.interface, graph.config);
graphModel
.addProp('data', [])
.addMethod('clear', () => {
graph.config.data = [];
return graph.interface;
})
.addMethod('addPoint', p => {
graph.config.data.push(p);
return graph.interface;
})
.addPropFunctor('x', null)
.addPropFunctor('y', null)
.addPropFunctor('color', null)
.addPropFunctor('row', () => undefined);
}
return graph.interface;
};
return tooltip;
}