// 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 }); }));