1 (function (global, factory) {
2 typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
3 typeof define === 'function' && define.amd ? define(factory) :
4 (global.StudyComparison = factory());
5 }(this, (function () { 'use strict';
7 // creates a new comparison object
11 // size options and things
19 var link_maker = null;
21 var rsquare_format = d3.format(".3f");
23 function accessionAccessor(a){
24 return d3.mean(a.variables[opts.trait])
27 var observationUnit_data = [];
31 // sets the variable to be used for the histogram and comparison grid
32 scomp.setVariable = function(variable){
33 opts.trait = variable;
37 scomp.links = function(link_m){
43 scomp.setOpt = function(opt,val){
48 scomp.getOpt = function(opt){
52 // loads and parses an array of observationUnits to be displayed in the comparison
53 // returns a list of compareable (shared) traits.
54 scomp.loadData = function(observationUnits){
55 console.log("loads",observationUnits);
56 observationUnit_data = observationUnits.slice(0);
58 var nester = d3.nest().key(function(unit){
59 return unit.studyDbId;
60 }).key(function(unit){
61 return unit.germplasmDbId;
64 var studyVariables = {};
65 var nested = nester.entries(observationUnits);
66 nested.forEach(function(study){
67 study.accessions = {};
68 var thisStudyVariables = {};
69 study.values.forEach(function(accession){
70 study.accessions[accession.key] = accession;
71 accession.variables = {};
72 accession.values.forEach(function(unit){
73 unit.observations.forEach(function(obs){
74 if (accession.variables[obs.observationVariableName]==undefined) {
75 accession.variables[obs.observationVariableName] = [];
76 thisStudyVariables[obs.observationVariableName] = true;
78 accession.variables[obs.observationVariableName].push(obs.value);
82 d3.keys(thisStudyVariables).forEach(function(k){
83 studyVariables[k] = studyVariables[k]?studyVariables[k]+1:1;
88 var sharedVars = d3.keys(studyVariables).filter(function(k){
89 return studyVariables[k]==nested.length;
93 for (var row = 0; row < nested.length-1; row++) {
95 for (var col = 1; col < nested.length; col++) {
97 paired_grid[row].push([nested[row],nested[col]]);
100 paired_grid[row].push(null);
107 grid_data = paired_grid;
111 // draws a histogram.
112 scomp.multiHist = function(selector){
113 // set up svg canvas and create layers
114 var prev = d3.select(selector)
115 .selectAll(".mulhst-svg")
117 prev.exit().remove();
118 var newSvg = prev.enter()
120 .classed("mulhst-svg",true)
121 .attr("width",opts.size+opts.axes)
122 .attr("height",opts.size+opts.axes)
123 .attr("viewBox","0 0 "+(opts.size+opts.axes)+" "+(opts.size+opts.axes));
125 .classed("mulhst-main",true)
126 .attr("transform","translate(0,"+opts.axes+")");
127 newSvg.append("g").classed("mulhst-xaxis",true)
128 .attr("transform","translate(0,"+(opts.size)+")"); newSvg.append("g")
129 .classed("mulhst-yaxis",true)
130 .attr("transform","translate("+opts.axes+","+opts.axes+")");
131 var lgnd = newSvg.append("g")
132 .classed("mulhst-legend",true);
133 lgnd.append("rect").classed("mulhst-legend-bg",true)
134 .attr("fill","white").attr("fill-opacity",0.65)
135 .attr("stroke","black")
136 .attr("x",-opts.margins).attr("y",-opts.margins);
137 lgnd.append("g").classed("mulhst-legend-main",true);
138 var builtSvg = newSvg.merge(prev);
139 var main = builtSvg.select(".mulhst-main");
140 scomp.mhist = builtSvg.node();
141 var xaxis = builtSvg.select(".mulhst-xaxis");
142 var yaxis = builtSvg.select(".mulhst-yaxis");
143 var legend = builtSvg.select(".mulhst-legend");
146 // filter and bin accessions
147 var allaccessions = hist_data.reduce(function(a,d){return a.concat(d3.values(d.accessions))},[]);
148 var total_extent = d3.extent(allaccessions,accessionAccessor);
150 var x = d3.scaleLinear().domain(total_extent).range([opts.axes,(opts.size-opts.margins)]).nice(bin_guess);
151 var histogram = d3.histogram().domain(x.domain()).thresholds(x.ticks(bin_guess)).value(accessionAccessor);
153 // create bins for each study at each position
157 hist_data.forEach(function(study){
158 var studyAccessions = d3.values(study.accessions);
159 var studyBins = histogram(studyAccessions)
160 .filter(function(bin){return bin.length!=0;});
161 studyBins.forEach(function(bin){
164 study_ids.push(study.key);
165 bins.push.apply(bins, studyBins);
168 // set up colors and the y-axis
169 var color = d3.scaleOrdinal(d3.schemeCategory10).domain(study_ids);
170 var y = d3.scaleLinear().domain([0,d3.max(bins,function(bin){return bin.length})])
171 .range([(opts.size-opts.axes),opts.margins]);
173 // Perform kernel estimations. Values are normalized to prevent the
174 // order of magnitude from affecting the smoothing degree.
175 var kernelNormY = y.copy();
176 kernelNormY.range([0,100]);
177 var kernelNormX = x.copy();
178 kernelNormX.range([0,100]);
179 hist_data.forEach(function(study){
180 var studyAccessions = d3.values(study.accessions);
181 console.log("a",x.ticks(bin_guess).map(kernelNormX));
182 console.log("b",studyAccessions.map(accessionAccessor).map(kernelNormX));
185 'path':kernelDensityEstimator(kernelEpanechnikov(10), x.ticks(bin_guess).map(kernelNormX))
186 (studyAccessions.map(accessionAccessor).map(kernelNormX)).map(function(d){
187 return [d[0],d[1]*1000]
193 var xax = d3.axisBottom(x);
195 var yax = d3.axisLeft(y);
198 // draw bars and lower
199 var bars = main.selectAll(".mulhst-bar").data(bins);
200 bars.exit().remove();
201 var newBars = bars.enter().append("g").classed("mulhst-bar",true);
202 newBars.append("rect").attr("opacity",0.5);
203 var allBars = newBars.merge(bars).order();
204 allBars.select("rect")
205 .attr("x",function(d){
208 .attr("y",function(d){
211 .attr("width",function(d){
212 return x(d.x1) - x(d.x0)-1
214 .attr("height",function(d){
215 return y(0)-y(d.length)
217 .attr("fill",function(d){
218 return color(d.study.key);
222 // kernel line function
223 var kernelCurve = d3.line().curve(d3.curveBasis)
224 .x(function(d){return x(kernelNormX.invert(d[0]));})
225 .y(function(d){return y(kernelNormY.invert(d[1]));});
227 // draw kernel estimation backgrounds (white border)
228 var lineBGs = main.selectAll(".mulhst-kernel-bg").data(kernels);
229 lineBGs.exit().remove();
230 var newLines = lineBGs.enter().append("path").classed("mulhst-kernel-bg",true);
231 var allLines = newLines.merge(lineBGs)
232 .attr("d",function(d){
234 return kernelCurve(d.path);
236 .attr("stroke","white")
238 .attr("stroke-width",4)
239 .attr("stroke-opacity",0.75)
242 // draw kernel estimation curves
243 var lines = main.selectAll(".mulhst-kernel").data(kernels);
244 lines.exit().remove();
245 var newLines = lines.enter().append("path").classed("mulhst-kernel",true);
246 var allLines = newLines.merge(lines)
247 .attr("d",function(d){
249 return kernelCurve(d.path);
251 .attr("stroke",function(d){
252 return color(d.study.key);
255 .attr("stroke-width",2)
256 .attr("stroke-opacity",0.8)
260 var legents = legend.select(".mulhst-legend-main").selectAll(".mulhst-legend-entry").data(hist_data);
261 legents.exit().remove();
262 var newLegents = legents.enter().append("g").classed("mulhst-legend-entry",true);
263 newLegents.append("circle")
264 .attr("fill-opacity",0.6)
265 .attr("stroke-width",2)
266 .attr("cx",8).attr("cy",8).attr("r",8);
267 newLegents.append("text").attr("font-size",12).attr("x",22).attr("y",13);
268 var allLegents = newLegents.merge(legents);
269 allLegents.attr("transform",function(d,i){
270 return d3.zoomIdentity.translate(0,22*i);
272 .on("click",scomp.multiHist.toggle);
273 allLegents.select("circle").attr("fill",function(d){
276 .attr("stroke",function(d){
279 allLegents.select("text").text(function(d){
280 return d3.values(d.accessions)[0].values[0].studyName
283 // transform legend into upper-right corner
284 var bbox = legend.select(".mulhst-legend-main").node().getBBox();
285 var bgwidth = bbox.width+opts.margins*2;
286 var bgheight = bbox.height+opts.margins*2;
287 legend.select(".mulhst-legend-bg")
288 .attr("width",bgwidth)
289 .attr("height",bbox.height+opts.margins*2);
290 legend.attr("transform","translate("+(opts.size-bgwidth)+","+(opts.margins*2+opts.axes)+")");
293 // set the histogram to show specfic studies
294 scomp.multiHist.showStudies = function(studies){
295 if(!scomp.mhist)return; // this function affects the histogram, if it doesnt exist, exit
297 var fadet = d3.transition();
298 d3.select(scomp.mhist).selectAll(".mulhst-bar")
300 .style("opacity",function(d){
301 return studies.indexOf(d.study)!=-1?null:0;
303 d3.select(scomp.mhist).selectAll(".mulhst-bar,.mulhst-kernel,.mulhst-kernel-bg")
305 .style("opacity",function(d){
306 return studies.indexOf(d.study)!=-1?null:0;
308 d3.select(scomp.mhist).selectAll(".mulhst-legend-entry")
309 .attr("isNotShown",function(d){
310 return studies.indexOf(d)!=-1?null:true;
314 .style("fill-opacity",function(d){
315 return studies.indexOf(d)!=-1?null:0;
319 // toggle the visibility of a study (this is a click event on the legend item)
320 scomp.multiHist.toggle = function(toggleTo){
321 if(!scomp.mhist)return; // this function affects the histogram, if it doesnt exist, exit
323 // resets attributes to match selected.
324 d3.select(scomp.mhist).selectAll(".mulhst-legend-entry")
325 .attr("isDeselected",function(){
326 var curr = d3.select(this).attr("isNotShown");
327 return curr?true:null;
330 var entry = d3.select(this);
331 if (!(toggleTo===true||toggleTo===false)){
332 toggleTo = !entry.attr("isDeselected");
334 entry.attr("isDeselected",function(){
335 return toggleTo?true:null;
337 scomp.multiHist.showSelected();
340 // resets the histogram to what the user manually selected (via clicking)
341 scomp.multiHist.showSelected = function(){
342 if(!scomp.mhist)return; // this function affects the histogram, if it doesnt exist, exit
344 var selected = d3.select(scomp.mhist).selectAll(".mulhst-legend-entry")
346 return !d3.select(this).attr("isDeselected");
348 .nodes().map(function(node){
349 return d3.select(node).datum()
351 scomp.multiHist.showStudies(selected);
355 // draws the comparison grid
356 scomp.graphGrid = function(selector){
359 var prev = d3.select(selector)
360 .selectAll(".grapgr-svg")
362 prev.exit().remove();
363 var newSvg = prev.enter()
365 .classed("grapgr-svg",true)
366 .attr("width",opts.size+opts.axes)
367 .attr("height",opts.size+opts.axes)
368 .attr("viewBox","0 0 "+(opts.size+opts.axes)+" "+(opts.size+opts.axes));
370 .attr("transform","translate("+opts.axes+","+opts.axes+")")
372 .classed("grapgr-main",true);
374 .classed("grapgr-topaxis",true)
375 .attr("transform","translate("+(opts.axes+opts.margins)+","+(opts.axes+opts.margins)+")");
377 .classed("grapgr-leftaxis",true)
378 .attr("transform","translate("+(opts.axes+opts.margins)+","+(opts.axes+opts.margins)+")");
379 var builtSvg = newSvg.merge(prev);
380 var main = builtSvg.select(".grapgr-main")
381 .attr("transform","translate(0, 0) scale(1,1)");
383 var taxis = builtSvg.select(".grapgr-topaxis");
384 var laxis = builtSvg.select(".grapgr-leftaxis");
386 var tooltip = newSvg.append("a")
387 .classed("grapgr-tooltip",true)
389 .attr('transform', 'translate(' + 0 + ',' + -100 + ')')
390 .attr("target","_blank");
391 tooltip.append('rect')
392 .attr('x',-2).attr('y',-15)
395 .attr('fill','black');
396 tooltip.append('text')
397 .attr('fill','white')
398 .attr('text-decoration','underline')
401 .attr('font-size','12');
402 tooltip = tooltip.merge(d3.select('.grapgr-tooltip'));
403 var tooltip_timeout = false;
404 var tooltip_hold = false;
405 function set_tooltip(viz,x,y,text,link){
406 if (tooltip_hold) return;
407 console.log(viz,x,y,text);
408 var tt = d3.select('.grapgr-tooltip');
409 var ttrect = tt.select('rect');
410 var tttext = tt.select('text');
412 if (!tooltip_timeout) tooltip_timeout = setTimeout(function(){
413 tooltip_timeout = false;
414 if (tooltip_hold) return;
415 tt.attr('opacity',0);
416 tt.attr('transform', 'translate(' + 0 + ',' + -100 + ')');
420 clearTimeout(tooltip_timeout);
421 tooltip_timeout = false;
422 if (tt.attr('opacity')==1 && text!==undefined && tttext.text()==text) return;
423 tt.attr('opacity',1);
424 tt.attr('transform', 'translate(' + x + ',' + y + ')');
426 ttrect.attr('width',tttext.node().getComputedTextLength()+6);
428 tt.attr('href',link).style('cursor','pointer');
430 tt.attr('href',null).on('click',function(){return false;}).style('cursor','auto');
433 tooltip.on('mousemove',function(){
436 tooltip.on('mouseout',function(){
441 // calculate grid size
442 var cellSize = (opts.size-(opts.margins*(grid_data.length+1)))/grid_data.length;
443 var cellOffset = (opts.size-opts.margins)/grid_data.length;
446 var rows = main.selectAll(".grapgr-row").data(function(d){return d;});
447 var newRows = rows.enter().append("g").classed("grapgr-row",true);
448 rows.exit().remove();
449 var allRows = newRows.merge(rows)
450 .attr("ryp",function(d,i){return i;})
451 .attr("transform",function(d,i){
452 var y = opts.margins+cellOffset*i;
453 var x = opts.margins;
454 return "translate("+x+","+y+")";
458 var cells = allRows.selectAll(".grapgr-cell").data(function(d){return d;});
459 var newCells = cells.enter().append("g").classed("grapgr-cell",true);
460 newCells.append("rect").classed("grapgr-cell-bg",true)
461 .attr("fill","white")
462 .attr("width",opts.size)
463 .attr("height",opts.size)
464 .attr("stroke","black");
465 newCells.append("g").classed("grapgr-graph-points",true);
466 newCells.append("g").classed("grapgr-graph-xaxis",true)
467 .attr('transform', 'translate(0,'+(opts.size-opts.axes)+')');
468 newCells.append("g").classed("grapgr-graph-yaxis",true)
469 .attr('transform', 'translate('+(opts.axes)+',0)');
470 cells.exit().remove();
471 var allCells = newCells.merge(cells)
473 .attr("cxp",function(d,i){return i;})
474 .attr("cyp",function(d,i){return d3.select(this.parentNode).attr("ryp");})
475 .attr("transform",function(d,i){
476 var x = cellOffset*i;
478 var s = cellSize/opts.size;
479 return "translate("+x+","+y+") scale("+s+")";
482 // sort null and drawable cells
483 var nullCells = allCells.filter(function(d){return d==null;});
484 var drawCells = allCells.filter(function(d){return d!=null;});
487 drawCells.each(function(d,i){
492 // find accessions which are measured in both studies
493 d3.entries(xStudy.accessions).forEach(function(kv){
494 accessions[kv.key] = {'xObs':kv.value};
495 accessions[kv.key].xVals = kv.value.variables[opts.trait];
496 if (accessions[kv.key].xVals){
497 accessions[kv.key].xAvg = d3.mean(accessions[kv.key].xVals);
500 d3.entries(yStudy.accessions).forEach(function(kv){
501 if (accessions[kv.key]){
502 accessions[kv.key].yObs = kv.value;
503 accessions[kv.key].yVals = kv.value.variables[opts.trait];
504 if (accessions[kv.key].yVals){
505 accessions[kv.key].yAvg = d3.mean(accessions[kv.key].yVals);
509 var accessions = d3.values(accessions).filter(function(a){
510 return a.xVals&&a.yVals&&a.xAvg!=undefined&&a.yAvg!=undefined;
513 // set the cell to null if there are no matching accessions.
514 if (accessions.length<2){
515 d3.select(this).datum(null);
519 // find the x/y extents and the total extent, create scales
520 var xtent = d3.extent(accessions,function(d){return d.xAvg});
521 var ytent = d3.extent(accessions,function(d){return d.yAvg});
522 var ttent = d3.extent(xtent.concat(ytent));
523 var x = d3.scaleLinear().domain(ttent).range([opts.axes,(opts.size-opts.margins)]);
524 var y = d3.scaleLinear().domain(ttent).range([opts.size-opts.axes,opts.margins]);
527 var xaxF = d3.axisBottom(x);
528 var xaxis = d3.select(this).select(".grapgr-graph-xaxis").call(xaxF);
529 var yaxF = d3.axisLeft(y);
530 var yaxis = d3.select(this).select(".grapgr-graph-yaxis").call(yaxF);
533 var pointLayer = d3.select(this).select(".grapgr-graph-points");
534 var points = pointLayer.selectAll(".grapgr-graph-point").data(accessions);
535 var newPoints = points.enter().append("circle")
536 .classed("grapgr-graph-point",true)
538 .attr("stroke","red")
539 .attr("stroke-width",4)
540 .attr("stroke-opacity",0.1)
542 points.exit().remove();
543 var allPoints = newPoints.merge(points)
544 .attr("cx",function(d){
547 .attr("cy",function(d){
551 allPoints.on('mouseover',function(d){
552 var newLoc = d3.mouse(d3.select('.grapgr-tooltip').node().parentNode);
553 var name = d.xObs.values[0].germplasmName;
554 var link = link_maker?link_maker(d.xObs.values[0].germplasmDbId):undefined;
555 set_tooltip(true,newLoc[0],newLoc[1],name,link);
556 }).on('mouseout',function(){
561 var regression = leastSquares(
562 accessions.map(function(d){return d.xAvg}),
563 accessions.map(function(d){return d.yAvg}));
565 .x(function(d){return x(d[0]);})
566 .y(function(d){return y(d[1]);});
568 var yentint = regression.slope*ttent[0]+regression.yintercept;
569 var xentint = (ttent[0]-regression.yintercept)/regression.slope;
571 if ((yentint < xentint && regression.slope>0) || (yentint > xentint && regression.slope<0) ){
572 pathd.push([xentint,ttent[0]]);
575 pathd.push([ttent[0],yentint]);
577 var youtint = regression.slope*ttent[1]+regression.yintercept;
578 var xoutint = (ttent[1]-regression.yintercept)/regression.slope;
579 if ((xoutint < youtint && regression.slope>0) || (xoutint > youtint && regression.slope<0) ){
580 pathd.push([xoutint,ttent[1]]);
583 pathd.push([ttent[1],youtint]);
585 var tline = d3.select(this).selectAll(".grapgr-graph-tline").data([pathd]);
586 tline.exit().remove();
587 tline.enter().append("path")
588 .classed("grapgr-graph-tline",true).merge(tline)
589 .attr("stroke","blue")
590 .attr("d",function(d){return line(d)});
592 var rsquare = d3.select(this).selectAll(".grapgr-graph-rsquare").data([regression.rSquare]);
593 rsquare.exit().remove();
594 rsquare.enter().append("text")
595 .classed("grapgr-graph-rsquare",true).merge(rsquare)
596 .attr("stroke","none")
598 .attr("transform","translate("+(opts.axes+14)+","+(opts.margins+14)+")")
599 .text(function(d){return "r\u00B2 = "+rsquare_format(d)});
602 // sort null and drawable cells again, accounting for missmatched studies
603 nullCells = allCells.filter(function(d){return d==null;});
604 drawCells = allCells.filter(function(d){return d!=null;});
606 // color placeholder and recolor former placeholders
607 nullCells.selectAll(".grapgr-cell>* *, .grapgr-graph-tline, .grapgr-graph-rsquare").remove();
608 nullCells.on("click",null)
609 .select(".grapgr-cell-bg")
610 .attr("fill","#f8f8f8")
611 .attr("stroke","#ddd");
612 drawCells.select(".grapgr-cell-bg")
613 .attr("fill","white")
614 .attr("stroke","black");
616 // makes comparison axes
617 var topLabels = grid_data[0].map(function(d){
618 return d3.values(d[1].accessions)[0].values[0].studyName
620 var topLabelPos = grid_data[0].map(function(d,i){
621 return cellOffset*i + cellSize/2;
623 var topScale = d3.scaleOrdinal()
626 var topaxis = d3.axisTop(topScale);
627 taxis.selectAll("*").remove();
628 taxis.call(topaxis).attr("font-size",12);
629 taxis.select(".domain").style("opacity",0);
630 taxis.selectAll(".tick>line").style("opacity",0);
631 taxis.selectAll(".tick>line").style("opacity",0);
632 var leftLabels = grid_data.map(function(d){
633 return d3.values(d[d.length-1][0].accessions)[0].values[0].studyName
635 var leftLabelPos = grid_data.map(function(d,i){
636 return cellOffset*i + cellSize/2;
638 var leftScale = d3.scaleOrdinal()
640 .range(leftLabelPos);
641 var leftaxis = d3.axisLeft(leftScale);
642 laxis.selectAll("*").remove();
643 laxis.call(leftaxis).attr("font-size",12);
644 laxis.select(".domain").style("opacity",0);
645 laxis.selectAll(".tick>line").style("opacity",0);
646 laxis.selectAll(".tick>text")
647 .attr("text-anchor","middle")
648 .attr("transform","rotate(-90) translate(9,-12)");
650 // if we have a grid larger than 1x1, enable zooming/selecting!
651 if(grid_data.length>1){
652 drawCells.on("click",cellZoomIn);
655 // zooms in on a cell and selects the relevant histogram series
656 function cellZoomIn(d){
658 drawCells.on("click",null);
659 var k = (opts.size-2*opts.margins)/cellSize;
660 var margin_offset = opts.margins - opts.margins/k;
661 var xPos = d3.select(this).attr("cxp")*cellOffset + margin_offset;
662 var yPos = d3.select(this).attr("cyp")*cellOffset + margin_offset;
664 .on("end",function(){
665 drawCells.on("click",cellZoomOut);
667 .attr("transform",d3.zoomIdentity.scale(k).translate(-xPos,-yPos))
668 .selectAll(".grapgr-cell")
669 .filter(function(d){return self!=this;})
671 topScale.range(topLabelPos.map(function(v){return (v-xPos+margin_offset)*k}));
672 taxis.transition().call(topaxis);
673 leftScale.range(leftLabelPos.map(function(v){return (v-yPos+margin_offset)*k}));
674 laxis.transition().call(leftaxis);
675 scomp.multiHist.showStudies(d);
678 // zooms out, resets histogram
679 function cellZoomOut(d){
680 drawCells.on("click",null);
682 .on("end",function(){
683 drawCells.on("click",cellZoomIn);
685 .attr("transform",d3.zoomIdentity)
686 .selectAll(".grapgr-cell")
688 topScale.range(topLabelPos);
689 taxis.transition().call(topaxis);
690 leftScale.range(leftLabelPos);
691 laxis.transition().call(leftaxis);
692 scomp.multiHist.showSelected();
700 // regression (http://bl.ocks.org/benvandyke/8459843)
701 function leastSquares(xSeries, ySeries) {
702 var reduceSumFunc = function(prev, cur) { return prev + cur; };
704 var xBar = xSeries.reduce(reduceSumFunc) * 1.0 / xSeries.length;
705 var yBar = ySeries.reduce(reduceSumFunc) * 1.0 / ySeries.length;
707 var ssXX = xSeries.map(function(d) { return Math.pow(d - xBar, 2); })
708 .reduce(reduceSumFunc);
710 var ssYY = ySeries.map(function(d) { return Math.pow(d - yBar, 2); })
711 .reduce(reduceSumFunc);
713 var ssXY = xSeries.map(function(d, i) { return (d - xBar) * (ySeries[i] - yBar); })
714 .reduce(reduceSumFunc);
716 var slope = ssXY / ssXX;
717 var yintercept = yBar - (xBar * slope);
718 var xintercept = (-yintercept)/slope;
719 var rSquare = Math.pow(ssXY, 2) / (ssXX * ssYY);
721 return {'slope':slope, 'yintercept':yintercept, 'xintercept':xintercept, 'rSquare':rSquare};
724 // kernel estimation (https://bl.ocks.org/mbostock/4341954)
725 function kernelDensityEstimator(kernel, X) {
727 return X.map(function(x) {
728 return [x, d3.mean(V, function(v) { return kernel(x - v); })];
732 function kernelEpanechnikov(k) {
734 return Math.abs(v /= k) <= 1 ? 0.75 * (1 - v * v) / k : 0;