// https://github.com/micahstubbs/d3-voronoi-scatterplot Version 0.1.0. Copyright 2016 Contributors.
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('d3'), require('lodash')) :
typeof define === 'function' && define.amd ? define(['exports', 'd3', 'lodash'], factory) :
(factory((global.d3VoronoiScatterplot = global.d3VoronoiScatterplot || {}),global.d3,global._));
}(this, function (exports,d3,_) { 'use strict';
_ = 'default' in _ ? _['default'] : _;
function d3Tip () {
// Mappings to new version of d3
var d3$$ = {
select: d3.select,
event: function event() {
return d3.event;
},
selection: d3.selection,
functor: function functor(v) {
return typeof v === "function" ? v : function () {
return v;
};
}
};
var direction = d3_tip_direction,
offset = d3_tip_offset,
html = d3_tip_html,
node = initNode(),
svg = null,
point = null,
target = null,
parent = null;
function tip(vis) {
svg = getSVGNode(vis);
point = svg.createSVGPoint();
}
// Public - show the tooltip on the screen
//
// Returns a tip
tip.show = function () {
if (!parent) tip.parent(document.body);
var args = Array.prototype.slice.call(arguments);
// console.log('args from tip.show', args);
if (args[args.length - 1] instanceof SVGElement) target = args.pop();
var content = html.apply(this, args),
poffset = offset.apply(this, args),
dir = direction.apply(this, args),
nodel = getNodeEl(),
i = directions.length,
coords,
parentCoords = node.offsetParent.getBoundingClientRect();
nodel.html(content).style('position', 'absolute').style('opacity', 1).style('pointer-events', 'all');
while (i--) {
nodel.classed(directions[i], false);
}coords = direction_callbacks[dir].apply(this);
nodel.classed(dir, true).style('top', coords.top + poffset[0] - parentCoords.top + 'px').style('left', coords.left + poffset[1] - parentCoords.left + 'px');
// .style('top', (coords.top - parentCoords.top + 'px')
// .style('left', (coords.left - parentCoords.left) + 'px')
return tip;
};
// Public - hide the tooltip
//
// Returns a tip
tip.hide = function () {
var nodel = getNodeEl();
nodel.style('opacity', 0).style('pointer-events', 'none');
return tip;
};
// Public: Proxy attr calls to the d3 tip container. Sets or gets attribute value.
//
// n - name of the attribute
// v - value of the attribute
//
// Returns tip or attribute value
tip.attr = function (n, v) {
if (arguments.length < 2 && typeof n === 'string') {
return getNodeEl().attr(n);
} else {
var args = Array.prototype.slice.call(arguments);
d3$$.selection.prototype.attr.apply(getNodeEl(), args);
}
return tip;
};
// Public: Proxy style calls to the d3 tip container. Sets or gets a style value.
//
// n - name of the property
// v - value of the property
//
// Returns tip or style property value
tip.style = function (n, v) {
// debugger;
if (arguments.length < 2 && typeof n === 'string') {
return getNodeEl().style(n);
} else {
var args = Array.prototype.slice.call(arguments);
if (args.length === 1) {
var styles = args[0];
Object.keys(styles).forEach(function (key) {
d3$$.selection.prototype.style.apply(getNodeEl(), [key, styles[key]]);
});
}
}
return tip;
};
// Public: Sets or gets the parent of the tooltip element
//
// v - New parent for the tip
//
// Returns parent element or tip
tip.parent = function (v) {
if (!arguments.length) return parent;
parent = v || document.body;
// console.log('parent from tip.parent', parent);
parent.appendChild(node);
// Make sure offsetParent has a position so the tip can be
// based from it. Mainly a concern with
.
var offsetParent = d3$$.select(node.offsetParent);
if (offsetParent.style('position') === 'static') {
offsetParent.style('position', 'relative');
}
return tip;
};
// Public: Set or get the direction of the tooltip
//
// v - One of n(north), s(south), e(east), or w(west), nw(northwest),
// sw(southwest), ne(northeast) or se(southeast)
//
// Returns tip or direction
tip.direction = function (v) {
if (!arguments.length) return direction;
direction = v == null ? v : d3$$.functor(v);
return tip;
};
// Public: Sets or gets the offset of the tip
//
// v - Array of [x, y] offset
//
// Returns offset or
tip.offset = function (v) {
if (!arguments.length) return offset;
offset = v == null ? v : d3$$.functor(v);
return tip;
};
// Public: sets or gets the html value of the tooltip
//
// v - String value of the tip
//
// Returns html value or tip
tip.html = function (v) {
if (!arguments.length) return html;
html = v == null ? v : d3$$.functor(v);
return tip;
};
// Public: destroys the tooltip and removes it from the DOM
//
// Returns a tip
tip.destroy = function () {
if (node) {
getNodeEl().remove();
node = null;
}
return tip;
};
function d3_tip_direction() {
return 'n';
}
function d3_tip_offset() {
return [0, 0];
}
function d3_tip_html() {
return ' ';
}
var direction_callbacks = {
n: direction_n,
s: direction_s,
e: direction_e,
w: direction_w,
nw: direction_nw,
ne: direction_ne,
sw: direction_sw,
se: direction_se
};
var directions = Object.keys(direction_callbacks);
function direction_n() {
var bbox = getScreenBBox();
return {
top: bbox.n.y - node.offsetHeight,
left: bbox.n.x - node.offsetWidth / 2
};
}
function direction_s() {
var bbox = getScreenBBox();
return {
top: bbox.s.y,
left: bbox.s.x - node.offsetWidth / 2
};
}
function direction_e() {
var bbox = getScreenBBox();
return {
top: bbox.e.y - node.offsetHeight / 2,
left: bbox.e.x
};
}
function direction_w() {
var bbox = getScreenBBox();
return {
top: bbox.w.y - node.offsetHeight / 2,
left: bbox.w.x - node.offsetWidth
};
}
function direction_nw() {
var bbox = getScreenBBox();
return {
top: bbox.nw.y - node.offsetHeight,
left: bbox.nw.x - node.offsetWidth
};
}
function direction_ne() {
var bbox = getScreenBBox();
return {
top: bbox.ne.y - node.offsetHeight,
left: bbox.ne.x
};
}
function direction_sw() {
var bbox = getScreenBBox();
return {
top: bbox.sw.y,
left: bbox.sw.x - node.offsetWidth
};
}
function direction_se() {
var bbox = getScreenBBox();
return {
top: bbox.se.y,
left: bbox.e.x
};
}
function initNode() {
var node = d3$$.select(document.createElement('div'));
node.style('position', 'absolute').style('top', 0).style('opacity', 0).style('pointer-events', 'none').style('box-sizing', 'border-box');
return node.node();
}
function getSVGNode(el) {
el = el.node();
if (el.tagName.toLowerCase() === 'svg') return el;
return el.ownerSVGElement;
}
function getNodeEl() {
if (node === null) {
node = initNode();
// re-add node to DOM
document.body.appendChild(node);
};
return d3$$.select(node);
}
// Private - gets the screen coordinates of a shape
//
// Given a shape on the screen, will return an SVGPoint for the directions
// n(north), s(south), e(east), w(west), ne(northeast), se(southeast), nw(northwest),
// sw(southwest).
//
// +-+-+
// | |
// + +
// | |
// +-+-+
//
// Returns an Object {n, s, e, w, nw, sw, ne, se}
function getScreenBBox() {
var targetel = target || d3$$.event().target;
while ('undefined' === typeof targetel.getScreenCTM && 'undefined' === targetel.parentNode) {
targetel = targetel.parentNode;
}
var bbox = {},
matrix = targetel.getScreenCTM(),
tbbox = targetel.getBBox(),
width = tbbox.width,
height = tbbox.height,
x = tbbox.x,
y = tbbox.y;
point.x = x;
point.y = y;
bbox.nw = point.matrixTransform(matrix);
point.x += width;
bbox.ne = point.matrixTransform(matrix);
point.y += height;
bbox.se = point.matrixTransform(matrix);
point.x -= width;
bbox.sw = point.matrixTransform(matrix);
point.y -= height / 2;
bbox.w = point.matrixTransform(matrix);
point.x += width;
bbox.e = point.matrixTransform(matrix);
point.x -= width / 2;
point.y -= height / 2;
bbox.n = point.matrixTransform(matrix);
point.y += height;
bbox.s = point.matrixTransform(matrix);
return bbox;
}
return tip;
};
function tooltip(tooltipVariables) {
var tip = d3Tip().parent(document.getElementById('chart')).attr('class', 'd3-tip').html(function (d) {
// console.log('d from tooltip html function', d);
var allRows = '';
tooltipVariables.forEach(function (e) {
var currentValue = void 0;
if (typeof e.format !== 'undefined') {
if (e.type === 'time') {
// time formatting
var inputValue = new Date(Number(d.datum[e.name]));
// TODO: handle case where date values are strings
var currentFormat = d3.timeFormat(e.format);
currentValue = currentFormat(inputValue);
} else {
// number formatting
var _inputValue = Number(d.datum[e.name]);
var _currentFormat = d3.format(e.format);
currentValue = _currentFormat(_inputValue);
}
} else {
// no formatting
currentValue = d.datum[e.name];
}
var currentRow = '' + e.name + ' ' + currentValue + '';
allRows = allRows.concat(currentRow);
});
return '\n ' + allRows + '\n
';
});
return tip;
}
function d3DistanceLimitedVoronoi() {
/////// Internals ///////
var voronoi = d3.voronoi().extent([[-1e6, -1e6], [1e6, 1e6]]);
var limit = 20; // default limit
var context = null; // set it to render to a canvas' 2D context
function _distanceLimitedVoronoi(data) {
if (context != null) {
//renders into a Canvas
context.beginPath();
voronoi.polygons(data).forEach(function (cell) {
distanceLimitedCell(cell, limit, context);
});
return true;
} else {
//final viz is an SVG
return voronoi.polygons(data).map(function (cell) {
return {
path: distanceLimitedCell(cell, limit, d3.path()).toString(),
datum: cell.data
};
});
}
}
///////////////////////
///////// API /////////
///////////////////////
_distanceLimitedVoronoi.limit = function (_) {
if (!arguments.length) {
return limit;
}
if (typeof _ === "number") {
limit = Math.abs(_);
}
return _distanceLimitedVoronoi;
};
_distanceLimitedVoronoi.x = function (_) {
if (!arguments.length) {
return voronoi.x();
}
voronoi.x(_);
return _distanceLimitedVoronoi;
};
_distanceLimitedVoronoi.y = function (_) {
if (!arguments.length) {
return voronoi.y();
}
voronoi.y(_);
return _distanceLimitedVoronoi;
};
_distanceLimitedVoronoi.extent = function (_) {
if (!arguments.length) {
return voronoi.extent();
}
voronoi.extent(_);
return _distanceLimitedVoronoi;
};
//exposes the underlying d3.geom.voronoi
//eg. allows to code 'limitedVoronoi.voronoi().triangle(data)'
_distanceLimitedVoronoi.voronoi = function (_) {
if (!arguments.length) {
return voronoi;
}
voronoi = _;
return _distanceLimitedVoronoi;
};
_distanceLimitedVoronoi.context = function (_) {
if (!arguments.length) {
return context;
}
context = _;
return _distanceLimitedVoronoi;
};
///////////////////////
/////// Private ///////
///////////////////////
function distanceLimitedCell(cell, r, context) {
var seed = [voronoi.x()(cell.data), voronoi.y()(cell.data)];
if (allVertecesInsideMaxDistanceCircle(cell, seed, r)) {
context.moveTo(cell[0][0], cell[0][1]);
for (var j = 1, m = cell.length; j < m; ++j) {
context.lineTo(cell[j][0], cell[j][1]);
}
context.closePath();
return context;
} else {
var pathNotYetStarted = true;
var firstPointTooFar = pointTooFarFromSeed(cell[0], seed, r);
var p0TooFar = firstPointTooFar;
var p0, p1, intersections;
var openingArcPoint, lastClosingArcPoint;
var startAngle, endAngle;
//begin: loop through all segments to compute path
for (var iseg = 0; iseg < cell.length; iseg++) {
p0 = cell[iseg];
p1 = cell[(iseg + 1) % cell.length];
// compute intersections between segment and maxDistance circle
intersections = segmentCircleIntersections(p0, p1, seed, r);
// complete the path (with lines or arc) depending on:
// intersection count (0, 1, or 2)
// if the segment is the first to start the path
// if the first point of the segment is inside or outside of the maxDistance circle
if (intersections.length === 2) {
if (p0TooFar) {
if (pathNotYetStarted) {
pathNotYetStarted = false;
// entire path will finish with an arc
// store first intersection to close last arc
lastClosingArcPoint = intersections[0];
// init path at 1st intersection
context.moveTo(intersections[0][0], intersections[0][1]);
} else {
//draw arc until first intersection
startAngle = angle(seed, openingArcPoint);
endAngle = angle(seed, intersections[0]);
context.arc(seed[0], seed[1], r, startAngle, endAngle, 1);
}
// then line to 2nd intersection, then initiliaze an arc
context.lineTo(intersections[1][0], intersections[1][1]);
openingArcPoint = intersections[1];
} else {
// THIS CASE IS IMPOSSIBLE AND SHOULD NOT ARISE
console.error("What's the f**k");
}
} else if (intersections.length === 1) {
if (p0TooFar) {
if (pathNotYetStarted) {
pathNotYetStarted = false;
// entire path will finish with an arc
// store first intersection to close last arc
lastClosingArcPoint = intersections[0];
// init path at first intersection
context.moveTo(intersections[0][0], intersections[0][1]);
} else {
// draw an arc until intersection
startAngle = angle(seed, openingArcPoint);
endAngle = angle(seed, intersections[0]);
context.arc(seed[0], seed[1], r, startAngle, endAngle, 1);
}
// then line to next point (1st out, 2nd in)
context.lineTo(p1[0], p1[1]);
} else {
if (pathNotYetStarted) {
pathNotYetStarted = false;
// init path at p0
context.moveTo(p0[0], p0[1]);
}
// line to intersection, then initiliaze arc (1st in, 2nd out)
context.lineTo(intersections[0][0], intersections[0][1]);
openingArcPoint = intersections[0];
}
p0TooFar = !p0TooFar;
} else {
if (p0TooFar) {
// entire segment too far, nothing to do
true;
} else {
// entire segment in maxDistance
if (pathNotYetStarted) {
pathNotYetStarted = false;
// init path at p0
context.moveTo(p0[0], p0[1]);
}
// line to next point
context.lineTo(p1[0], p1[1]);
}
}
} //end: loop through all segments
if (pathNotYetStarted) {
// special case: no segment intersects the maxDistance circle
// cell perimeter is entirely outside the maxDistance circle
// path is the maxDistance circle
pathNotYetStarted = false;
context.moveTo(seed[0] + r, seed[1]);
context.arc(seed[0], seed[1], r, 0, 2 * Math.PI, false);
} else {
// if final segment ends with an opened arc, close it
if (firstPointTooFar) {
startAngle = angle(seed, openingArcPoint);
endAngle = angle(seed, lastClosingArcPoint);
context.arc(seed[0], seed[1], r, startAngle, endAngle, 1);
}
context.closePath();
}
return context;
}
function allVertecesInsideMaxDistanceCircle(cell, seed, r) {
var result = true;
var p;
for (var ip = 0; ip < cell.length; ip++) {
result = result && !pointTooFarFromSeed(cell[ip], seed, r);
}
return result;
}
function pointTooFarFromSeed(p, seed, r) {
return Math.pow(p[0] - seed[0], 2) + Math.pow(p[1] - seed[1], 2) > Math.pow(r, 2);
}
function angle(seed, p) {
var v = [p[0] - seed[0], p[1] - seed[1]];
// from http://stackoverflow.com/questions/2150050/finding-signed-angle-between-vectors, with v1 = horizontal radius = [seed[0]+r - seed[0], seed[0] - seed[0]]
return Math.atan2(v[1], v[0]);
}
}
function segmentCircleIntersections(A, B, C, r) {
/*
from http://stackoverflow.com/questions/1073336/circle-line-segment-collision-detection-algorithm
*/
var Ax = A[0],
Ay = A[1],
Bx = B[0],
By = B[1],
Cx = C[0],
Cy = C[1];
// compute the euclidean distance between A and B
var LAB = Math.sqrt(Math.pow(Bx - Ax, 2) + Math.pow(By - Ay, 2));
// compute the direction vector D from A to B
var Dx = (Bx - Ax) / LAB;
var Dy = (By - Ay) / LAB;
// Now the line equation is x = Dx*t + Ax, y = Dy*t + Ay with 0 <= t <= 1.
// compute the value t of the closest point to the circle center (Cx, Cy)
var t = Dx * (Cx - Ax) + Dy * (Cy - Ay);
// This is the projection of C on the line from A to B.
// compute the coordinates of the point E on line and closest to C
var Ex = t * Dx + Ax;
var Ey = t * Dy + Ay;
// compute the euclidean distance from E to C
var LEC = Math.sqrt(Math.pow(Ex - Cx, 2) + Math.pow(Ey - Cy, 2));
// test if the line intersects the circle
if (LEC < r) {
// compute distance from t to circle intersection point
var dt = Math.sqrt(Math.pow(r, 2) - Math.pow(LEC, 2));
var tF = t - dt; // t of first intersection point
var tG = t + dt; // t of second intersection point
var result = [];
if (tF > 0 && tF < LAB) {
// test if first intersection point in segment
// compute first intersection point
var Fx = (t - dt) * Dx + Ax;
var Fy = (t - dt) * Dy + Ay;
result.push([Fx, Fy]);
}
if (tG > 0 && tG < LAB) {
// test if second intersection point in segment
// compute second intersection point
var Gx = (t + dt) * Dx + Ax;
var Gy = (t + dt) * Dy + Ay;
result.push([Gx, Gy]);
}
return result;
} else {
// either (LEC === r), tangent point to circle is E
// or (LEC < r), line doesn't touch circle
// in both cases, returning nothing is OK
return [];
}
}
return _distanceLimitedVoronoi;
};
function drawVoronoiOverlay(selector, data, options) {
/*
Initiate the Voronoi function
Use the same variables of the data in the .x and .y as used
in the cx and cy of the circle call
The clip extent will make the boundaries end nicely along
the chart area instead of splitting up the entire SVG
(if you do not do this it would mean that you already see
a tooltip when your mouse is still in the axis area, which
is confusing)
*/
var xVariable = options.xVariable;
var yVariable = options.yVariable;
var xScale = options.xScale;
var yScale = options.yScale;
var width = options.width;
var height = options.height;
var tip = options.tip;
var idVariable = options.idVariable;
if (typeof idVariable === 'undefined') idVariable = 'id';
var xAccessor = function xAccessor(d) {
return xScale(d[xVariable]);
};
var yAccessor = function yAccessor(d) {
return yScale(d[yVariable]);
};
var limitedVoronoi = d3DistanceLimitedVoronoi().x(xAccessor).y(yAccessor).limit(50).extent([[0, 0], [width, height]]);
// console.log('data[0]', data[0]);
// console.log('data from drawVoronoiOverlay', data);
var xValues = data.map(function (d) {
return d[xVariable];
});
// console.log('current xVariable', xVariable);
// console.log('xValues', xValues);
var yValues = data.map(function (d) {
return d[yVariable];
});
// console.log('current yVariable', yVariable);
// console.log('yValues', yValues);
var limitedVoronoiCells = limitedVoronoi(data);
// remove any existing Voronoi overlay
selector.selectAll('.voronoiWrapper').remove();
// create a group element to place the Voronoi diagram in
var limitedVoronoiGroup = selector.append('g').attr('class', 'voronoiWrapper');
// Create the distance-limited Voronoi diagram
limitedVoronoiGroup.selectAll('path').data(limitedVoronoiCells) // Use Voronoi() with your dataset inside
.enter().append('path')
// .attr("d", function(d, i) { return "M" + d.join("L") + "Z"; })
.attr('d', function (d) {
// console.log('d from limitedVoronoiGroup', d);
if (typeof d !== 'undefined') {
return d.path;
}
return '';
})
// Give each cell a unique class where the unique part corresponds to the circle classes
// .attr('class', d => `voronoi ${d.datum[idVariable]}`)
.attr('class', function (d) {
if (typeof d !== 'undefined') {
if (typeof d.datum[idVariable] !== 'undefined') {
return 'voronoi id' + xVariable + yVariable + d.datum[idVariable];
}
return 'voronoi id' + xVariable + yVariable + d[idVariable];
}
return 'voronoi';
}).style('stroke', 'lightblue') // I use this to look at how the cells are dispersed as a check
// .style('stroke', 'none')
.style('fill', 'none').style('pointer-events', 'all')
// .on('mouseover', tip.show)
// .on('mouseout', tip.hide);
.on('mouseover', function (d, i, nodes) {
// console.log('d from mouseover', d);
// console.log('i from mouseover', i);
// console.log('nodes from mouseover', nodes);
// console.log('this from mouseover', this);
showTooltip(d, i, nodes);
}).on('mouseout', function (d, i, nodes) {
// console.log('this from mouseout', this);
removeTooltip(d, i, nodes);
});
// Show the tooltip on the hovered over circle
function showTooltip(d, i, nodes) {
// Save the circle element (so not the voronoi which is triggering the hover event)
// in a variable by using the unique class of the voronoi (idVariable)
var elementSelector = void 0;
if (typeof d.datum[idVariable] !== 'undefined') {
elementSelector = '.marks.id' + xVariable + yVariable + d.datum[idVariable];
} else {
elementSelector = '.marks.id' + xVariable + yVariable + d[idVariable];
}
// console.log('elementSelector', elementSelector);
var element = void 0;
if (typeof d.datum[idVariable] !== 'undefined') {
element = d3.selectAll('.marks.id' + xVariable + yVariable + d.datum[idVariable]);
} else {
element = d3.selectAll('.marks.id' + xVariable + yVariable + d[idVariable]);
}
// console.log('element from showTooltip', element);
// console.log('d from showTooltip', d);
var pathStartX = Number(d.path.split('M')[1].split(',')[0]);
var pathStartY = Number(d.path.split(',')[1].split('L')[0]);
// console.log('pathStartX', pathStartX);
// console.log('pathStartY', pathStartY);
// console.log('element.nodes()[0] from showTooltip', element.nodes()[0]);
var currentDOMNode = element.nodes()[0];
var cx = currentDOMNode.cx.baseVal.value;
var cy = currentDOMNode.cy.baseVal.value;
tip.show(d, i, nodes);
// const tipTop = tip.style('top');
// const tipLeft = tip.style('left');
// const tipTopValue = Number(tipTop.slice(0, -2));
// const tipLeftValue = Number(tipLeft.slice(0, -2));
// const offsetX = tipLeftValue - cx;
// const offsetY = tipTopValue - cy;
var offsetX = 0; // pathStartX + (pathStartX - cx);
var offsetY = pathStartY + (pathStartY - cy);
// console.log('cx', cx);
// console.log('tipLeft', tipLeft);
// console.log('tipLeftValue', tipLeftValue);
// console.log('calculated offsetX', offsetX);
// console.log('cy', cy);
// console.log('tipTop', tipTop);
// console.log('tipTopValue', tipTopValue);
// console.log('calculated offsetY', offsetY);
// tip.offset([offsetX,offsetY]);
// tip.offset([150, 150]);
// Make chosen circle more visible
element.style('fill-opacity', 1);
} // function showTooltip
// Hide the tooltip when the mouse moves away
function removeTooltip(d, i, nodes) {
// Save the circle element (so not the voronoi which is triggering the hover event)
// in a variable by using the unique class of the voronoi (idVariable)
var element = void 0;
if (typeof d.datum[idVariable] !== 'undefined') {
element = d3.selectAll('.marks.id' + xVariable + yVariable + d.datum[idVariable]);
} else {
element = d3.selectAll('.marks.id' + xVariable + yVariable + d[idVariable]);
}
// console.log('element from removeTooltip', element);
// console.log('element.nodes()[0] from removeTooltip', element.nodes()[0]);
var currentDOMNode = element.nodes()[0];
tip.hide(d, i, nodes);
// Fade out the bright circle again
element.style('fill-opacity', 0.3);
} // function removeTooltip
}
function drawVoronoiScatterplot(selector, inputData, options) {
//
// Set-up
//
// vanilla JS window width and height
var wV = window;
var dV = document;
var eV = dV.documentElement;
var gV = dV.getElementsByTagName('body')[0];
var xV = wV.innerWidth || eV.clientWidth || gV.clientWidth;
var yV = wV.innerHeight || eV.clientHeight || gV.clientHeight;
// Quick fix for resizing some things for mobile-ish viewers
var mobileScreen = xV < 500;
// set default configuration
var cfg = {
margin: { left: 120, top: 20, right: 80, bottom: 20 },
width: 1000,
animateFromXAxis: undefined,
hideXLabel: undefined,
yVariable: 'y',
idVariable: undefined,
marks: {
r: 2,
fillOpacity: 0.3
}
};
// Put all of the options into a variable called cfg
if (typeof options !== 'undefined') {
for (var i in options) {
if (typeof options[i] !== 'undefined') {
cfg[i] = options[i];
}
} // for i
} // if
// console.log('options passed in to scatterplot', options);
// console.log('cfg from scatterplot', cfg);
// map variables to our dataset
var xVariable = cfg.xVariable;
var yVariable = cfg.yVariable;
var rVariable = undefined;
var idVariable = cfg.idVariable;
var groupByVariable = cfg.groupByVariable;
var wrapperId = cfg.wrapperId;
var wrapperLabel = cfg.wrapperLabel;
var tooltipVariables = cfg.tooltipColumns;
var numericVariables = cfg.numericColumns;
var xLabelDetail = cfg.xLabelDetail;
var hideXLabel = cfg.hideXLabel;
var xLabelTransform = cfg.xLabelTransform;
var yLabelTransform = cfg.yLabelTransform;
var dependent = cfg.dependent;
var globalExtents = cfg.globalExtents;
var animateFromXAxis = cfg.animateFromXAxis;
var opacityCircles = cfg.marks.fillOpacity;
var marksRadius = cfg.marks.r;
var dynamicWidth = cfg.dynamicWidth;
// labels
var xLabel = cfg.xLabel || xVariable;
if (typeof xLabelDetail !== 'undefined') {
xLabel = xLabel + ' (' + xLabelDetail + ')';
}
var yLabel = cfg.yLabel || yVariable;;
// const xLabel = 'y\u{0302}'; // y-hat for the prediction
// const yLabel = 'r\u{0302}'; // r-hat for the residual
var div = d3.select(selector).append('div').attr('id', 'chart');
// Scatterplot
var margin = cfg.margin;
var chartWidth = document.getElementById('chart').offsetWidth;
var height = cfg.width * 0.25;
// const maxDistanceFromPoint = 50;
var width = void 0;
if (typeof dynamicWidth !== 'undefined') {
// use a dynamic width derived from the window width
width = chartWidth - margin.left - margin.right;
} else {
// use width specified in the options passed in
width = cfg.width - margin.left - margin.right;
}
var svg = div.append('svg').attr('width', width + margin.left + margin.right).attr('height', height + margin.top + margin.bottom);
var wrapper = svg.append('g').classed('chartWrapper', true).classed('' + xVariable, true).attr('transform', 'translate(' + margin.left + ', ' + margin.top + ')');
if (typeof dependent !== 'undefined') {
svg.classed('dependent', true);
wrapper.classed('dependent', true);
wrapper.attr('id', wrapperId);
// draw model label
wrapper.append('g').attr('transform', 'translate(' + 20 + ', ' + 45 + ')').append('text').classed('modelLabel', true).style('font-size', '40px').style('font-weight', 400).style('opacity', 0.15).style('fill', 'gray').style('font-family', 'Work Sans, sans-serif').text('' + wrapperLabel);
} else {
svg.classed('independent', true);
wrapper.classed('independent', true);
wrapper.attr('id', wrapperId);
}
//
// Initialize Axes & Scales
//
// Set the color for each region
var color = d3.scaleOrdinal().range(['#1f78b4', '#ff7f00', '#33a02c', '#e31a1c', '#6a3d9a', '#b15928', '#a6cee3', '#fdbf6f', '#b2df8a', '#fb9a99', '#cab2d6', '#ffff99']);
// parse strings to numbers
var data = _.cloneDeep(inputData);
// console.log('data from scatterplot', data);
data.forEach(function (d, i) {
numericVariables.forEach(function (e) {
d[e] = Number(d[e]);
});
if (typeof idVariable === 'undefined') {
data[i].id = '' + i;
}
});
if (typeof idVariable === 'undefined') idVariable = 'id';
// console.log('data from drawVoronoiScatterplot', data);
//
// Scales
//
// Set the new x axis range
var xScale = d3.scaleLinear().range([0, width]);
// Set the new y axis range
var yScale = d3.scaleLinear().range([height, 0]);
if (typeof globalExtents !== 'undefined') {
// retrieve global extents
var xExtent = globalExtents[0];
var yExtent = globalExtents[1];
// set scale domains with global extents
xScale.domain(xExtent);
yScale.domain(yExtent).nice();
} else {
// set scale domains from the local extent
xScale.domain(d3.extent(data, function (d) {
return d[xVariable];
}));
// .nice();
yScale.domain(d3.extent(data, function (d) {
return d[yVariable];
})).nice();
}
// console.log('yScale.domain()', yScale.domain());
//
// Axes
//
// Set new x-axis
var xAxis = d3.axisBottom().ticks(4).tickSizeOuter(0)
// .tickFormat(d => // Difficult function to create better ticks
// xScale.tickFormat((mobileScreen ? 4 : 8), e => {
// const prefix = d3.format(',.0s');
// return `${prefix(e)}`;
// })(d))
.scale(xScale);
// calculate y-position we'd like for the x-axis
var xAxisYTranslate = d3.max([0, yScale.domain()[0]]);
// Append the x-axis
wrapper.append('g').attr('class', 'x axis').attr('transform', 'translate(' + 0 + ', ' + yScale(xAxisYTranslate) + ')').call(xAxis);
var yAxis = d3.axisLeft().ticks(6) // Set rough # of ticks
.scale(yScale);
// Append the y-axis
wrapper.append('g').attr('class', 'y axis').attr('transform', 'translate(' + 0 + ', ' + 0 + ')').call(yAxis);
// Scale for the bubble size
if (typeof rVariable !== 'undefined') {
var _rScale = d3.scaleSqrt().range([mobileScreen ? 1 : 2, mobileScreen ? 10 : 16]).domain(d3.extent(data, function (d) {
return d[rVariable];
}));
}
//
// Tooltips
//
var tip = tooltip(tooltipVariables);
svg.call(tip);
//
// Scatterplot Circles
//
// Initiate a group element for the circles
var circleGroup = wrapper.append('g').attr('class', 'circleWrapper');
function update(data, options) {
console.log('update function was called');
// console.log('data from update function', data);
// handle NaN values
data = data.filter(function (d) {
return !Number.isNaN(d[xVariable]) && !Number.isNaN(d[yVariable]);
});
// an extra delay to allow large
// amounts of points time to render
var marksDelay = 0;
if (typeof options !== 'undefined') {
marksDelay = options.marksDelay;
// if a new groupByVariable is passed in, use it
if (typeof options.groupByVariable !== 'undefined') {
groupByVariable = options.groupByVariable;
};
}
// Place the circles
var updateSelection = circleGroup.selectAll('circle') // circleGroup.selectAll('.marks')
.data(function () {
if (typeof rVariable !== 'undefined') {
// Sort so the biggest circles are below
return data.sort(function (a, b) {
return b[rVariable] > a[rVariable];
});
}
return data;
}, function (d) {
return d[idVariable];
});
// console.log('updateSelection', updateSelection);
var enterSelection = updateSelection.enter().append('circle');
// console.log('enterSelection', enterSelection);
var exitSelection = updateSelection.exit();
// console.log('exitSelection', exitSelection);
updateSelection;
// .style('fill', 'black');
enterSelection.attr('class', function (d) {
return 'marks id' + xVariable + yVariable + d[idVariable];
}).style('fill-opacity', 0).style('fill', function (d) {
// console.log('d from style', d);
if (typeof groupByVariable !== 'undefined') {
return color(d[groupByVariable]);
}
return color.range()[0]; // 'green'
}).attr('cx', function (d) {
// console.log('cx parameters from drawVoronoiScatterplot');
// console.log('xScale', xScale);
// console.log('d', d);
// console.log('xVariable', xVariable);
// console.log('xScale(d[xVariable])', xScale(d[xVariable]));
return xScale(d[xVariable]);
}).attr('cy', function (d) {
if (typeof animateFromXAxis !== 'undefined') {
return yScale(xAxisYTranslate);
} else {
return yScale(d[yVariable]);
}
}).attr('r', function (d) {
if (typeof rVariable !== 'undefined') {
return rScale(d[rVariable]);
}
return marksRadius;
}).transition().delay(marksDelay).duration(2000).style('fill-opacity', opacityCircles);
// .append('title')
// .text(d => `${d[idVariable]} ${d[xLabelDetail]}`);
exitSelection.transition().delay(marksDelay).duration(0).style('fill', 'lightgray') // 'red'
.transition().delay(2000).duration(2000).style('fill-opacity', 0).remove();
var mergedSelection = updateSelection.merge(enterSelection);
// console.log('mergedSelection', mergedSelection);
// console.log('mergedSelection.nodes()', mergedSelection.nodes());
var mergedSelectionData = mergedSelection.nodes().map(function (d) {
return d.__data__;
});
// console.log('mergedSelectionData', mergedSelectionData);
if (typeof animateFromXAxis !== 'undefined') {
updateSelection.transition().delay(2000).duration(2000).attr('cy', function (d) {
return yScale(d[yVariable]);
});
}
//
// distance-limited Voronoi overlay
//
var voronoiOptions = {
xVariable: xVariable,
yVariable: yVariable,
idVariable: idVariable,
xScale: xScale,
yScale: yScale,
width: width,
height: height,
tip: tip
};
drawVoronoiOverlay(wrapper, mergedSelectionData, voronoiOptions);
}
// call the update function once to kick things off
update(data);
//
// Initialize Labels
//
var xlabelText = xLabel || xVariable;
var yLabelText = yLabel || yVariable;
if (typeof hideXLabel === 'undefined') {
// Set up X axis label
var xTextAnchor = 'start';
var xLabelTranslate = void 0;
if (xLabelTransform === 'top') {
// label on top
xLabelTranslate = 'translate(' + 30 + ',' + -10 + ')';
} else if (typeof xLabelTransform !== 'undefined') {
// use specified [x, y, rotate] transform
xLabelTranslate = 'rotate(' + yLabelTransform[2] + ') translate(' + xLabelTransform[0] + ',' + xLabelTransform[1] + ')';
} else {
// default to no translation
xLabelTranslate = 'translate(' + width + ',' + (height - 10) + ')';
xTextAnchor = 'end';
}
wrapper.append('g').append('text').attr('class', 'x title').attr('text-anchor', xTextAnchor).style('font-size', (mobileScreen ? 8 : 12) + 'px').style('font-weight', 600).attr('transform', xLabelTranslate).text('' + xlabelText);
}
// Set up y axis label
var yLabelTranslate = void 0;
if (yLabelTransform === 'left') {
// label on the left
yLabelTranslate = 'translate(' + -(margin.left / 4) + ',' + yScale(xAxisYTranslate) + ')';
} else if (typeof yLabelTransform !== 'undefined') {
// use specified [x, y, rotate] transform
yLabelTranslate = 'rotate(' + yLabelTransform[2] + ') translate(' + yLabelTransform[0] + ',' + yLabelTransform[1] + ')';
} else {
// default
yLabelTranslate = 'rotate(270) translate(' + 0 + ',' + 10 + ')';
}
wrapper.append('g').append('text').attr('class', 'y title').attr('text-anchor', 'end').attr('dy', '0.35em').style('font-size', (mobileScreen ? 8 : 12) + 'px')
// .attr('transform', 'translate(18, 0) rotate(-90)')
.attr('transform', yLabelTranslate).text('' + yLabelText);
//
// Hide axes on click
//
var axisVisible = true;
function click() {
if (axisVisible) {
d3.selectAll('.y.axis').style('opacity', 0);
d3.selectAll('.x.axis text').style('opacity', 0);
d3.selectAll('.x.axis .tick').style('opacity', 0);
axisVisible = false;
} else {
d3.selectAll('.axis').style('opacity', 1);
d3.selectAll('.x.axis text').style('opacity', 1);
d3.selectAll('.x.axis .tick').style('opacity', 1);
axisVisible = true;
}
}
d3.selectAll('.chartWrapper').on('click', function () {
click();
});
// console.log('update from drawVoronoiScatterplot', update);
return update;
// drawVoronoiScatterplot.update = (data) => {
// // console.log('drawVoronoiScatterplot.update() was called');
// if (typeof update === 'function') update(data);
// };
}
exports.drawVoronoiScatterplot = drawVoronoiScatterplot;
Object.defineProperty(exports, '__esModule', { value: true });
}));