Skip to content

Instantly share code, notes, and snippets.

@asielen
Last active August 3, 2018 19:38
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save asielen/1a5e8d77ae8feb464167 to your computer and use it in GitHub Desktop.
Save asielen/1a5e8d77ae8feb464167 to your computer and use it in GitHub Desktop.
Violin Plot + Box Plot v2

Please use the newest version Violin Plot + Box Plot v3

An implementation of a reusable responsive distribution chart. Based on the concept outlined in Mike Bostocks blog post Towards Reusable Charts.

Features:

  • Responsive design, chart size adjusts with screen <Open in new window to see example.>
  • Easily styled in CSS
  • Modular design supporting 3 types of charts
    • Box Plot
    • Notched Box Plot
    • Violin Plot
    • Beeswarm Plot
    • Bean Plot
    • Trendlines
  • Each chart type supports multiple options and styles such as
    • Box width
    • Show/Hide any component (median line, mean line, whiskers outliers, etc...)
    • Scatter Outliers
    • Notch style (traditional angled vs 90 degree cutouts)
    • Violin resolution and interpolation
    • Scatter style (random vs organized beeswarm)

See this live on bl.ocks.org here.

Previous version: Reusable Violin + Box Plot

date value
1997 235.492658
2000 12.78922318
2002 272.467626
2002 119.6556657
1999 187.9846243
2001 69.47658426
2002 333.4844961
1997 30.48358982
1997 224.4668862
1999 78.26183787
2000 189.2755021
1998 91.12591737
2000 290.744577
2001 174.1963747
1996 103.0917701
2000 69.81017893
2001 480.291848
1999 85.43741588
1999 214.4428058
1999 107.6345675
2002 387.6592237
1997 -10.45641487
2000 174.3811667
1999 101.6902808
2000 288.941279
2000 36.24070644
2000 290.7807505
1998 -25.37467545
1999 176.0780333
1999 120.7779666
1996 109.1798011
2002 146.9643401
2002 355.1468772
2001 -9.8653231
1999 242.096719
1999 125.9216174
2000 227.1486649
2001 217.7912902
1999 211.2686444
2001 203.0963781
1999 189.5447805
1999 80.39835942
1998 131.640685
2000 173.2676982
2001 282.3938889
1997 58.37697598
1996 103.5791582
2002 147.1950849
1996 109.4897692
1998 60.8430612
2002 298.9029275
2002 105.63109
1998 204.953735
2001 49.55609438
2000 348.2355707
1996 -40.4655177
1996 213.0511689
2000 131.2765598
1996 123.8588332
1998 0.087462028
1999 204.8568513
2002 182.592293
2001 298.3562535
1999 87.46922994
2002 249.1324546
2000 165.0709314
2000 204.5126185
1997 54.47991361
2001 373.7843513
2001 133.5956344
1996 104.9850661
1999 106.468237
1999 241.2308921
2002 81.08528445
1997 118.9453665
2001 139.7477448
2002 290.6906809
2001 177.0870252
2000 189.3895132
1997 10.66302037
1996 142.2576696
1998 -3.536073131
1997 142.81951
1996 15.78226591
2001 620.5352219
1996 5.354662884
1999 212.0508616
2001 249.1113278
1999 184.5041038
1997 -25.1323592
1998 183.2779389
1999 78.60002251
2001 295.9738066
1998 59.01151537
1996 107.1088534
1999 74.77483109
1999 176.823566
2000 153.262423
2000 197.2815571
2002 157.3113529
2001 216.8918122
2002 56.43294327
2000 208.1190006
1998 104.917479
1998 202.3337623
2000 159.2816593
2001 283.7455604
1998 15.1992965
2001 306.5569413
2000 156.2405042
1997 154.6367318
2002 181.0815681
1997 117.167273
1999 131.7437731
1998 66.51937532
2002 190.4143255
1999 178.5670718
2001 116.5544154
1997 147.0771729
1997 30.26227614
1996 255.1018849
1996 31.19203737
2002 224.5700223
2002 210.3081146
1996 119.1265699
1999 130.2608334
1997 152.8029101
1998 56.98671769
1998 129.1905942
1996 11.16831935
1996 111.5356885
1998 92.07420722
1997 137.2888373
2001 117.4842369
2002 363.3560798
2002 101.7373686
1997 143.3097703
1996 -39.56899491
1998 168.777575
1998 55.51384739
1997 188.9888552
2000 92.86614561
1999 213.2956768
1998 26.59607897
2000 213.7065708
2001 189.2946982
2001 318.7838486
2001 24.98270412
2000 206.4720225
1997 30.33204204
2002 272.11071
2000 150.2997055
1998 206.7456497
1996 -179.5236746
1999 206.0643352
2000 79.90939158
1997 141.1983185
2000 115.2998739
1998 144.967381
2000 31.43777613
1998 159.7814459
1999 104.4879624
2002 332.1068389
2000 177.9224535
2001 378.5138582
1997 32.84751664
2000 117.6811897
1996 26.35513826
2001 259.0274252
2002 29.6750742
1999 188.0337954
2000 100.8909358
1998 150.2195155
1997 51.39077087
1997 255.2771028
1996 38.53534355
1999 227.4730755
2002 79.32895291
1997 147.4436656
1997 18.51298138
1998 184.496877
1998 52.98683772
2000 209.0935495
2002 65.43675518
2001 224.52009
2001 215.8760782
1999 208.1979795
2000 120.0860051
1997 158.2984045
1999 75.99538742
2002 217.1506813
2002 93.90349359
2000 269.1705505
1997 48.10125638
1996 120.516961
2000 80.33354928
1996 218.9661772
1997 21.26795744
1998 205.5449544
2000 157.0034833
1997 148.455503
1997 -167.5404806
1998 157.2561445
1999 128.1881518
2000 168.1362481
1998 86.35741537
1999 223.0230108
1998 20.98669767
2000 245.249197
1996 7.216533173
2002 239.3737133
1998 18.47167645
2002 369.2414993
1996 -55.84421223
1997 162.4213859
1997 251.9661491
2000 221.6187623
1997 27.32880447
2002 314.4829396
2000 97.3310802
1997 136.3695005
1998 40.68188509
2000 224.6124067
2002 -235.3372028
2000 263.1672647
2000 113.248023
1997 139.0255253
1996 -57.3064545
2001 311.2383989
1999 132.9215499
1998 169.6834317
2002 170.9177607
2002 320.4232805
2002 47.86797011
2001 402.1332649
2001 165.5504962
1999 159.3238552
2000 70.44559969
1999 241.6485651
1998 43.97248208
2001 393.0711189
2000 136.4441773
2001 184.338522
1999 116.0907822
1998 169.3930701
1999 83.94449361
1999 247.9036297
1997 70.29151431
1996 132.4200298
1999 81.19007363
2002 246.3438782
2000 135.7778671
2000 218.5585815
2000 84.852422
2002 253.6149145
2002 219.5528333
2001 281.2874347
1997 -7.924332662
2000 202.3672629
1997 17.8615395
1998 214.7904826
1998 108.2889175
1997 156.7199202
1997 32.27045889
1998 184.8776008
2000 107.7741666
2001 294.3131341
1996 235.8609511
2002 395.4988388
1997 25.77873569
2001 215.9997624
2000 124.1696818
2001 274.7008241
1998 108.3336315
1997 137.3364761
1997 -22.34572523
1996 146.2906155
1998 41.11264284
2000 278.0028974
1997 32.35667152
1996 219.7173038
2002 1.989301262
1996 112.4334102
2001 204.854325
1999 203.5237656
1997 24.99498359
1996 96.73830558
1998 54.62651078
1997 160.1456454
1998 32.67459898
1996 115.4907965
1998 29.07783286
1999 216.8251275
1999 78.59243493
1996 102.4626846
1998 16.72495134
1997 166.0643086
1998 69.35433571
1999 175.2699779
1996 3.895273084
2002 310.1405943
1996 -154.4888449
2000 200.6390783
1999 117.0589313
1997 166.1404766
2002 143.3767093
1999 207.237133
2001 118.5874084
1998 178.6003776
1999 136.1884927
1997 127.0194697
2002 80.78502483
1998 160.0101585
1997 43.11580155
2002 320.2293032
2001 63.9547117
1999 222.4589456
1999 100.2259714
1997 196.7200462
1998 45.10416596
2001 307.1761849
2002 52.54178692
1998 152.5717665
1998 51.91967191
2000 270.2152935
1998 16.75183077
2002 308.5483853
1998 30.78238414
1997 146.5208022
1997 44.90739225
2001 390.4017159
1996 -88.73883919
1998 155.4178898
1998 89.52826631
1999 226.3591181
1998 61.18384519
2002 460.2499835
1997 67.16201538
2000 188.5919537
2001 111.8162202
1996 234.4337289
2002 142.0918315
1998 181.5084118
1997 36.33278458
1997 115.4863315
2000 88.11897456
1999 216.3988883
2000 118.4842525
1997 293.7663673
1999 69.52447538
2001 284.5773491
2001 111.2517515
1997 126.8824221
2002 190.9875275
2000 269.371308
1998 68.82306265
1999 184.3286147
2001 179.0121473
2001 304.3377493
1996 14.62790124
1996 144.129878
1997 48.52233078
1997 157.6834272
1997 40.50527276
2001 231.518908
1999 83.3357254
2002 223.5890229
1996 3.216433629
2002 340.6858598
1998 23.92800719
2002 233.8925204
1996 20.83116595
1997 207.7008425
1998 21.66115597
1999 176.7110226
2000 82.84456463
2002 264.6316532
1997 -14.18915853
2001 312.6080087
2001 146.2458943
1996 180.9625683
1996 301.6592662
2001 306.2574888
1997 35.40632568
2002 387.9620358
1998 17.51968972
1999 179.3350507
1996 -31.46229666
2001 254.2793076
1997 24.78321378
2001 265.0277484
2000 125.0517638
2001 272.4304582
1999 134.9788404
2001 312.0306711
2001 93.72482107
2001 353.2259462
2002 203.7528321
2000 266.8474927
2000 152.744575
2000 214.1230196
1998 73.31246417
2001 390.216855
1996 -53.83835237
1999 182.6551439
1996 9.615146867
1999 218.5435378
1997 9.241989674
2002 338.1281241
2001 113.2937203
1998 162.0485339
1999 53.35605624
1998 191.1735842
1997 -32.70420142
2000 263.64273
1997 32.31930571
1998 146.2884959
2002 12.05637736
2002 348.4146764
1997 32.57248406
1997 159.3152939
2001 94.11331495
1996 113.8806642
1999 124.5645393
1996 98.60463857
1999 53.1980209
1997 134.0778784
1998 29.83573607
2001 282.1277857
1996 -114.3512762
2001 234.5388986
1999 86.61909045
1998 149.4956165
1996 -107.5528647
1999 171.4209724
2001 106.0516182
2002 349.8956266
2002 236.4416983
2000 189.0055767
1996 -2.28809019
2000 201.3450942
1998 22.19958039
1996 115.1873987
1998 79.07383951
1997 156.6084368
1999 103.584283
1998 160.5971799
2002 145.4487557
1997 140.2496033
2002 18.27371915
1997 119.812044
1996 23.10316392
1997 -53.78423529
1997 36.73485065
1998 184.8087369
2000 21.42067776
1997 348.5806097
1996 -30.2717926
2002 271.763914
1998 105.697728
1997 121.8292503
1999 78.487395
2002 235.4230647
2001 141.1374159
1998 164.7276978
2000 194.211953
1999 167.7457576
1998 48.07672475
1998 137.4174271
1996 1.727053752
1997 398.9537065
1998 36.95144679
1999 203.8726798
2001 146.8616848
1997 347.8589508
1997 26.64599875
2002 294.7474575
1997 9.228842433
1998 150.7376499
2001 177.7366385
1998 186.5734811
2001 111.586586
2001 318.1728591
2002 170.5915439
1996 93.91837066
2001 161.2858576
1996 398.3135273
1998 18.1052633
1999 228.630253
1996 -41.2136308
1996 113.8441256
1997 49.32396152
2002 270.011165
2001 357.3046536
1999 207.0001926
1996 0.485881957
1998 160.3471292
2000 195.725183
/*Primary Chart*/
/*Nested divs for responsiveness*/
.chart-wrapper {
max-width: 800px; /*Overwritten by the JS*/
min-width: 304px;
margin-bottom: 8px;
background-color: #FAF7F7;
}
.chart-wrapper .inner-wrapper {
position: relative;
padding-bottom: 50%; /*Overwritten by the JS*/
width: 100%;
}
.chart-wrapper .outer-box {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
}
.chart-wrapper .inner-box {
width: 100%;
height: 100%;
}
.chart-wrapper text {
font-family: sans-serif;
font-size: 13px;
}
.chart-wrapper .axis path,
.chart-wrapper .axis line {
fill: none;
stroke: #888;
stroke-width: 2px;
shape-rendering: crispEdges;
}
.chart-wrapper .y.axis .tick line {
stroke: lightgrey;
opacity: 0.6;
stroke-dasharray: 2,1;
stroke-width: 1;
shape-rendering: crispEdges;
}
.chart-wrapper .x.axis .domain {
display: none;
}
.chart-wrapper div.tooltip {
position: absolute;
text-align: left;
padding: 3px;
font: 12px sans-serif;
background: lightcyan;
border: 0px;
border-radius: 1px;
pointer-events: none;
opacity: 0.7;
}
/*Box Plot*/
.chart-wrapper .box-plot .box {
fill-opacity: 0.4;
stroke-width: 2;
}
.chart-wrapper .box-plot line {
stroke-width: 2px;
}
.chart-wrapper .box-plot circle {
fill: white;
stroke: black;
}
.chart-wrapper .box-plot .median {
stroke: black;
}
.chart-wrapper .box-plot circle.median {
/*the script makes the circles the same color as the box, you can override this in the js*/
fill: white !important;
}
.chart-wrapper .box-plot .mean {
stroke: white;
stroke-dasharray: 2,1;
stroke-width: 1px;
}
@media (max-width:500px){
.chart-wrapper .box-plot circle {display: none;}
}
/*Violin Plot*/
.chart-wrapper .violin-plot .area {
shape-rendering: geometricPrecision;
opacity: 0.4;
}
.chart-wrapper .violin-plot .line {
fill: none;
stroke-width: 2px;
shape-rendering: geometricPrecision;
}
/*Notch Plot*/
.chart-wrapper .notch-plot .notch {
fill-opacity: 0.4;
stroke-width: 2;
}
/* Point Plots*/
.chart-wrapper .points-plot .point {
stroke: black;
stroke-width: 1px;
}
.chart-wrapper .metrics-lines {
stroke-width: 4px;
}
/* Non-Chart Styles for demo*/
.chart-options {
min-width: 200px;
font-size: 13px;
font-family: sans-serif;
}
.chart-options button {
margin: 3px;
padding: 3px;
font-size: 12px;
}
.chart-options p {
display: inline;
}
@media (max-width:500px){
.chart-options p {display: block;}
}
/**
* @fileOverview A D3 based distribution chart system. Supports: Box plots, Violin plots, Notched box plots, trend lines, beeswarm plot
* @version 2.5
*/
/**
* Creates a box plot, violin plot, and or notched box plot
* @param settings Configuration options for the base plot
* @param settings.data The data for the plot
* @param settings.xName The name of the column that should be used for the x groups
* @param settings.yName The name of the column used for the y values
* @param {string} settings.selector The selector string for the main chart div
* @param [settings.axisLabels={}] Defaults to the xName and yName
* @param [settings.scale='linear'] 'linear' or 'log' - y scale of the chart
* @param [settings.chartSize={width:800, height:400}] The height and width of the chart itself (doesn't include the container)
* @param [settings.margin={top: 15, right: 60, bottom: 40, left: 50}] The margins around the chart (inside the main div)
* @param [settings.constrainExtremes=false] Should the y scale include outliers?
* @returns {object} chart A chart object
*/
function makeDistroChart(settings) {
var chart = {};
// Defaults
chart.settings = {
data: null,
xName: null,
yName: null,
selector: null,
axisLables: null,
scale:'linear',
chartSize:{width:800,height:400},
margin:{top: 15, right: 60, bottom: 40, left: 50},
constrainExtremes:false,
color:d3.scale.category10()};
for (var setting in settings) {chart.settings[setting] = settings[setting]}
function formatAsFloat(d) {
if (d % 1 !== 0) {
return d3.format(".2f")(d);
} else {
return d3.format(".0f")(d);
}
}
function logFormatNumber(d) {
var x = Math.log(d) / Math.log(10) + 1e-6;
return Math.abs(x - Math.floor(x)) < 0.6 ? formatAsFloat(d) : "";
}
chart.yFormatter = formatAsFloat;
chart.data = chart.settings.data;
chart.groupObjs = {}; //The data organized by grouping and sorted as well as any metadata for the groups
chart.objs = {mainDiv: null, chartDiv: null, g: null, xAxis: null, yAxis: null};
chart.colorFunct = null;
/**
* Takes an array, function, or object mapping and created a color function from it
* @param {function|[]|object} colorOptions
* @returns {function} Function to be used to determine chart colors
*/
function getColorFunct(colorOptions) {
if (typeof colorOptions == 'function') {
return colorOptions
} else if (Array.isArray(colorOptions)) {
// If an array is provided, map it to the domain
var colorMap = {}, cColor = 0;
for (var cName in chart.groupObjs) {
colorMap[cName] = colorOptions[cColor];
cColor = (cColor + 1) % colorOptions.length;
}
return function (group) {
return colorMap[group];
}
} else if (typeof colorOptions == 'object') {
// if an object is provided, assume it maps to the colors
return function (group) {
return colorOptions[group];
}
} else {
return d3.scale.category10();
}
}
/**
* Takes a percentage as returns the values that correspond to that percentage of the group range witdh
* @param objWidth Percentage of range band
* @param gName The bin name to use to get the x shift
* @returns {{left: null, right: null, middle: null}}
*/
function getObjWidth(objWidth, gName) {
var objSize = {left: null, right: null, middle: null};
var width = chart.xScale.rangeBand() * (objWidth / 100);
var padding = (chart.xScale.rangeBand() - width) / 2;
var gShift = chart.xScale(gName);
objSize.middle = chart.xScale.rangeBand() / 2 + gShift;
objSize.left = padding + gShift;
objSize.right = objSize.left + width;
return objSize;
}
/**
* Adds jitter to the scatter point plot
* @param doJitter true or false, add jitter to the point
* @param width percent of the range band to cover with the jitter
* @returns {number}
*/
function addJitter(doJitter, width) {
if (doJitter!==true || width==0) {return 0}
return Math.floor(Math.random() * width)-width/2;
}
function shallowCopy(oldObj) {
var newObj = {};
for(var i in oldObj) {
if(oldObj.hasOwnProperty(i)) {
newObj[i] = oldObj[i];
}
}
return newObj;
}
/**
* Closure that creates the tooltip hover function
* @param groupName Name of the x group
* @param metrics Object to use to get values for the group
* @returns {Function} A function that provides the values for the tooltip
*/
function tooltipHover(groupName, metrics) {
var tooltipString = "Group: " + groupName;
tooltipString += "<br\>Max: " + formatAsFloat(metrics.max, 0.1);
tooltipString += "<br\>Q3: " + formatAsFloat(metrics.quartile3);
tooltipString += "<br\>Median: " + formatAsFloat(metrics.median);
tooltipString += "<br\>Q1: " + formatAsFloat(metrics.quartile1);
tooltipString += "<br\>Min: " + formatAsFloat(metrics.min);
return function () {
chart.objs.tooltip.transition().duration(200).style("opacity", 0.9);
chart.objs.tooltip.html(tooltipString)
};
}
/**
* Parse the data and calculates base values for the plots
*/
!function prepareData() {
function calcMetrics(values) {
var metrics = { //These are the original non–scaled values
max: null,
upperOuterFence: null,
upperInnerFence: null,
quartile3: null,
median: null,
mean: null,
iqr: null,
quartile1: null,
lowerInnerFence: null,
lowerOuterFence: null,
min: null
};
metrics.min = d3.min(values);
metrics.quartile1 = d3.quantile(values, 0.25);
metrics.median = d3.median(values);
metrics.mean = d3.mean(values);
metrics.quartile3 = d3.quantile(values, 0.75);
metrics.max = d3.max(values);
metrics.iqr = metrics.quartile3 - metrics.quartile1;
//The inner fences are the closest value to the IQR without going past it (assumes sorted lists)
var LIF = metrics.quartile1 - (1.5 * metrics.iqr);
var UIF = metrics.quartile3 + (1.5 * metrics.iqr);
for (var i = 0; i <= values.length; i++) {
if (values[i] < LIF) {
continue;
}
if (!metrics.lowerInnerFence && values[i] >= LIF) {
metrics.lowerInnerFence = values[i];
continue;
}
if (values[i] > UIF) {
metrics.upperInnerFence = values[i - 1];
break;
}
}
metrics.lowerOuterFence = metrics.quartile1 - (3 * metrics.iqr);
metrics.upperOuterFence = metrics.quartile3 + (3 * metrics.iqr);
if (!metrics.lowerInnerFence) {
metrics.lowerInnerFence = metrics.min;
}
if (!metrics.upperInnerFence) {
metrics.upperInnerFence = metrics.max;
}
return metrics
}
var current_x = null;
var current_y = null;
var current_row;
// Group the values
for (current_row = 0; current_row < chart.data.length; current_row++) {
current_x = chart.data[current_row][chart.settings.xName];
current_y = chart.data[current_row][chart.settings.yName];
if (chart.groupObjs.hasOwnProperty(current_x)) {
chart.groupObjs[current_x].values.push(current_y);
} else {
chart.groupObjs[current_x] = {};
chart.groupObjs[current_x].values = [current_y];
}
}
for (var cName in chart.groupObjs) {
chart.groupObjs[cName].values.sort(d3.ascending);
chart.groupObjs[cName].metrics = {};
chart.groupObjs[cName].metrics = calcMetrics(chart.groupObjs[cName].values);
}
}();
/**
* Prepare the chart settings and chart div and svg
*/
!function prepareSettings() {
//Set base settings
chart.margin = chart.settings.margin;
chart.divWidth = chart.settings.chartSize.width;
chart.divHeight = chart.settings.chartSize.height;
chart.width = chart.divWidth - chart.margin.left - chart.margin.right;
chart.height = chart.divHeight - chart.margin.top - chart.margin.bottom;
if (chart.settings.axisLabels) {
chart.xAxisLable = chart.settings.axisLabels.xAxis;
chart.yAxisLable = chart.settings.axisLabels.yAxis;
} else {
chart.xAxisLable = chart.settings.xName;
chart.yAxisLable = chart.settings.yName;
}
if (chart.settings.scale === 'log') {
chart.yScale = d3.scale.log();
chart.yFormatter = logFormatNumber;
} else {
chart.yScale = d3.scale.linear();
}
if (chart.settings.constrainExtremes === true) {
var fences = [];
for (var cName in chart.groupObjs) {
fences.push(chart.groupObjs[cName].metrics.lowerInnerFence);
fences.push(chart.groupObjs[cName].metrics.upperInnerFence);
}
chart.range = d3.extent(fences);
} else {
chart.range = d3.extent(chart.data, function (d) {
return d[chart.settings.yName];
});
}
chart.colorFunct = getColorFunct(chart.settings.colors);
// Build Scale functions
chart.yScale.range([chart.height, 0]).domain(chart.range).nice().clamp(true);
chart.xScale = d3.scale.ordinal().domain(Object.keys(chart.groupObjs)).rangeBands([0, chart.width]);
//Build Axes Functions
chart.objs.yAxis = d3.svg.axis()
.scale(chart.yScale)
.orient("left")
.tickFormat(chart.yFormatter)
.outerTickSize(0)
.innerTickSize(-chart.width + (chart.margin.right + chart.margin.left));
chart.objs.xAxis = d3.svg.axis().scale(chart.xScale).orient("bottom").tickSize(5);
}();
/**
* Updates the chart based on the current settings and window size
* @returns {*}
*/
chart.update = function () {
// Update chart size based on view port size
chart.width = parseInt(chart.objs.chartDiv.style("width"), 10) - (chart.margin.left + chart.margin.right);
chart.height = parseInt(chart.objs.chartDiv.style("height"), 10) - (chart.margin.top + chart.margin.bottom);
// Update scale functions
chart.xScale.rangeBands([0, chart.width]);
chart.yScale.range([chart.height, 0]);
//Update axes
chart.objs.g.select('.x.axis').attr("transform", "translate(0," + chart.height + ")").call(chart.objs.xAxis)
.selectAll("text")
.attr("y", 5)
.attr("x", -5)
.attr("transform", "rotate(-45)")
.style("text-anchor", "end");
chart.objs.g.select('.x.axis .label').attr("x", chart.width / 2);
chart.objs.g.select('.y.axis').call(chart.objs.yAxis.innerTickSize(-chart.width));
chart.objs.g.select('.y.axis .label').attr("x", -chart.height / 2);
chart.objs.chartDiv.select('svg').attr("width", chart.width + (chart.margin.left + chart.margin.right)).attr("height", chart.height + (chart.margin.top + chart.margin.bottom));
return chart;
};
/**
* Prepare the chart html elements
*/
!function prepareChart() {
// Build main div and chart div
chart.objs.mainDiv = d3.select(chart.settings.selector)
.style("max-width", chart.divWidth + "px");
// Add all the divs to make it centered and responsive
chart.objs.mainDiv.append("div")
.attr("class", "inner-wrapper")
.style("padding-bottom", (chart.divHeight / chart.divWidth) * 100 + "%")
.append("div").attr("class", "outer-box")
.append("div").attr("class", "inner-box");
// Capture the inner div for the chart (where the chart actually is)
chart.selector = chart.settings.selector + " .inner-box";
chart.objs.chartDiv = d3.select(chart.selector);
d3.select(window).on('resize.' + chart.selector, chart.update);
// Create the svg
chart.objs.g = chart.objs.chartDiv.append("svg")
.attr("class", "chart-area")
.attr("width", chart.width + (chart.margin.left + chart.margin.right))
.attr("height", chart.height + (chart.margin.top + chart.margin.bottom))
.append("g")
.attr("transform", "translate(" + chart.margin.left + "," + chart.margin.top + ")");
// Create axes
chart.objs.axes = chart.objs.g.append("g").attr("class", "axis");
chart.objs.axes.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + chart.height + ")")
.call(chart.objs.xAxis);
chart.objs.axes.append("g")
.attr("class", "y axis")
.call(chart.objs.yAxis)
.append("text")
.attr("class", "label")
.attr("transform", "rotate(-90)")
.attr("y", -42)
.attr("x", -chart.height / 2)
.attr("dy", ".71em")
.style("text-anchor", "middle")
.text(chart.yAxisLable);
// Create tooltip div
chart.objs.tooltip = chart.objs.mainDiv.append('div').attr('class', 'tooltip');
for (var cName in chart.groupObjs) {
chart.groupObjs[cName].g = chart.objs.g.append("g").attr("class", "group");
chart.groupObjs[cName].g.on("mouseover", function () {
chart.objs.tooltip
.style("display", null)
.style("left", (d3.event.pageX) + "px")
.style("top", (d3.event.pageY - 28) + "px");
}).on("mouseout", function () {
chart.objs.tooltip.style("display", "none");
}).on("mousemove", tooltipHover(cName, chart.groupObjs[cName].metrics))
}
chart.update();
}();
/**
* Render a violin plot on the current chart
* @param options
* @param [options.showViolinPlot=true] True or False, show the violin plot
* @param [options.resolution=calculated based on values]
* @param [options.width=90] The max percent of the group rangeBand that the violin can be
* @param [options.interpolation=''] How to render the violin
* @param [options.colors=chart default] The color mapping for the violin plot
* @returns {*} The chart object
*/
chart.renderViolinPlot = function (options) {
chart.violinPlots = {};
//chart.violinPlots.plots = {};
var defaultOptions = {
show:true,
showViolinPlot:true,
resolution:null,
width:65,
interpolation:'basis-open',
colors:chart.colorFunct};
chart.violinPlots.options = shallowCopy(defaultOptions);
for (var option in options) {chart.violinPlots.options[option] = options[option]}
var vOpts = chart.violinPlots.options;
// Create violin plot objects
for (var cName in chart.groupObjs) {
chart.groupObjs[cName].violin = {};
chart.groupObjs[cName].violin.objs = {};
chart.groupObjs[cName].violin.histogramFunct = d3.layout.histogram().frequency(1);
}
/**
* Calculate the ideal number of bins from the cGroup's values
* @param cName
* @returns {number} Number of bins
*/
function calcNumBins(cName) {
var iqr;
if (chart.boxPlots) {
iqr = chart.groupObjs[cName].metrics.iqr
} else {
var quartile1 = d3.quantile(chart.groupObjs[cName].values, 0.25);
var quartile3 = d3.quantile(chart.groupObjs[cName].values, 0.75);
iqr = quartile3 - quartile1;
}
return Math.max(Math.round(2 * (iqr / Math.pow(chart.groupObjs[cName].values.length, 1 / 3))), 10)
}
/**
* Take a new set of options and redraw the violin
* @param updateOptions
*/
chart.violinPlots.change = function (updateOptions) {
if (updateOptions) {for (var key in updateOptions) {vOpts[key] = updateOptions[key]}}
for (var cName in chart.groupObjs) {chart.groupObjs[cName].violin.objs.g.remove()}
chart.violinPlots.prepareViolin();
chart.violinPlots.update()
};
chart.violinPlots.reset = function () {chart.violinPlots.change(defaultOptions)};
chart.violinPlots.show = function (opts) {
if (opts!==undefined) {
opts.show=true;
if (opts.reset) {chart.violinPlots.reset()}
} else {opts = {show:true};}
chart.violinPlots.change(opts)};
chart.violinPlots.hide = function (opts) {
if (opts!==undefined) {
opts.show=false;
if (opts.reset) {chart.violinPlots.reset()}
} else {opts = {show:false};}
chart.violinPlots.change(opts)};
/**
* Update the violin obj values
*/
chart.violinPlots.update = function () {
var cName, cViolinPlot;
for (cName in chart.groupObjs) {
cViolinPlot = chart.groupObjs[cName].violin;
if (vOpts.resolution) {
cViolinPlot.histogramFunct.bins(vOpts.resolution);
} else {
cViolinPlot.histogramFunct.bins(calcNumBins(cName));
}
cViolinPlot.histogramData = cViolinPlot.histogramFunct(chart.groupObjs[cName].values);
// Get the violin width
var objBounds = getObjWidth(vOpts.width, cName);
var width = (objBounds.right - objBounds.left) / 2;
// Build the violins sideways, so use the yScale for the xScale and make a new yScale
var xVScale = chart.yScale.copy();
var yVScale = d3.scale.linear()
.range([width, 0])
.domain([0,d3.max(cViolinPlot.histogramData, function (d) {return d.y;})])
.clamp(true);
var area = d3.svg.area()
.interpolate(vOpts.interpolation)
.x(function (d) {return xVScale(d.x);})
.y0(width)
.y1(function (d) {return yVScale(d.y);});
var line = d3.svg.line()
.interpolate(vOpts.interpolation)
.x(function (d) {return xVScale(d.x);})
.y(function (d) {return yVScale(d.y)});
if (cViolinPlot.objs.left.area) {
cViolinPlot.objs.left.area
.datum(cViolinPlot.histogramData)
.attr("d", area);
cViolinPlot.objs.left.line
.datum(cViolinPlot.histogramData)
.attr("d", line);
cViolinPlot.objs.right.area
.datum(cViolinPlot.histogramData)
.attr("d", area);
cViolinPlot.objs.right.line
.datum(cViolinPlot.histogramData)
.attr("d", line);
}
// Rotate the violins
cViolinPlot.objs.left.g.attr("transform", "rotate(90,0,0) translate(0,-" + objBounds.left + ") scale(1,-1)");
cViolinPlot.objs.right.g.attr("transform", "rotate(90,0,0) translate(0,-" + objBounds.right + ")");
}
};
/**
* Create the svg elements for the violin plot
*/
chart.violinPlots.prepareViolin = function() {
var cName, cViolinPlot;
if (vOpts.colors) {
chart.violinPlots.color = getColorFunct(vOpts.colors);
} else {
chart.violinPlots.color = chart.colorFunct
}
if (vOpts.show==false) {return}
for (cName in chart.groupObjs) {
cViolinPlot = chart.groupObjs[cName].violin;
cViolinPlot.objs.g = chart.groupObjs[cName].g.append("g").attr("class", "violin-plot");
cViolinPlot.objs.left = {area: null, line: null, g: null};
cViolinPlot.objs.right = {area: null, line: null, g: null};
cViolinPlot.objs.left.g = cViolinPlot.objs.g.append("g");
cViolinPlot.objs.right.g = cViolinPlot.objs.g.append("g");
if (vOpts.showViolinPlot !== false) {
//Area
cViolinPlot.objs.left.area = cViolinPlot.objs.left.g.append("path")
.attr("class", "area")
.style("fill", chart.violinPlots.color(cName));
cViolinPlot.objs.right.area = cViolinPlot.objs.right.g.append("path")
.attr("class", "area")
.style("fill", chart.violinPlots.color(cName));
//Lines
cViolinPlot.objs.left.line = cViolinPlot.objs.left.g.append("path")
.attr("class", "line")
.attr("fill", 'none')
.style("stroke", chart.violinPlots.color(cName));
cViolinPlot.objs.right.line = cViolinPlot.objs.right.g.append("path")
.attr("class", "line")
.attr("fill", 'none')
.style("stroke", chart.violinPlots.color(cName));
}
}
};
chart.violinPlots.prepareViolin();
d3.select(window).on('resize.' + chart.selector + '.violinPlot', chart.violinPlots.update);
chart.violinPlots.update();
return chart;
};
/**
* Render a box plot on the current chart
* @param options
* @param [options.show=true] Toggle the whole plot on and off
* @param [options.showBox=true] Show the box part of the box plot
* @param [options.showWhiskers=true] Show the whiskers
* @param [options.showMedian=true] Show the median line
* @param [options.showMean=false] Show the mean line
* @param [options.medianCSize=3] The size of the circle on the median
* @param [options.showOutliers=true] Plot outliers
* @param [options.boxwidth=30] The max percent of the group rangeBand that the box can be
* @param [options.lineWidth=boxWidth] The max percent of the group rangeBand that the line can be
* @param [options.outlierScatter=false] Spread out the outliers so they don't all overlap (in development)
* @param [options.outlierCSize=2] Size of the outliers
* @param [options.colors=chart default] The color mapping for the box plot
* @returns {*} The chart object
*/
chart.renderBoxPlot = function (options) {
chart.boxPlots = {};
// Defaults
var defaultOptions = {
show:true,
showBox:true,
showWhiskers:true,
showMedian:true,
showMean:false,
medianCSize:3.5,
showOutliers:true,
boxWidth:30,
lineWidth:null,
scatterOutliers:false,
outlierCSize:2.5,
colors:chart.colorFunct};
chart.boxPlots.options = shallowCopy(defaultOptions);
for (var option in options) {chart.boxPlots.options[option] = options[option]}
var bOpts = chart.boxPlots.options;
//Create box plot objects
for (var cName in chart.groupObjs) {
chart.groupObjs[cName].boxPlot = {};
chart.groupObjs[cName].boxPlot.objs = {};
}
/**
* Calculates all the outlier points for each group
*/
!function calcAllOutliers() {
/**
* Create lists of the outliers for each content group
* @param cGroup The object to modify
* @return null Modifies the object in place
*/
function calcOutliers(cGroup) {
var cExtremes = [];
var cOutliers = [];
var cOut, idx;
for (idx = 0; idx <= cGroup.values.length; idx++) {
cOut = {value: cGroup.values[idx]};
if (cOut.value < cGroup.metrics.lowerInnerFence) {
if (cOut.value < cGroup.metrics.lowerOuterFence) {
cExtremes.push(cOut);
} else {
cOutliers.push(cOut);
}
} else if (cOut.value > cGroup.metrics.upperInnerFence) {
if (cOut.value > cGroup.metrics.upperOuterFence) {
cExtremes.push(cOut);
} else {
cOutliers.push(cOut);
}
}
}
cGroup.boxPlot.objs.outliers = cOutliers;
cGroup.boxPlot.objs.extremes = cExtremes;
}
for (var cName in chart.groupObjs) {
calcOutliers(chart.groupObjs[cName]);
}
}();
/**
* Take updated options and redraw the box plot
* @param updateOptions
*/
chart.boxPlots.change = function (updateOptions) {
if (updateOptions) {for (var key in updateOptions) {bOpts[key] = updateOptions[key]}}
for (var cName in chart.groupObjs) {chart.groupObjs[cName].boxPlot.objs.g.remove()}
chart.boxPlots.prepareBoxPlot();
chart.boxPlots.update()
};
chart.boxPlots.reset = function () {chart.boxPlots.change(defaultOptions)};
chart.boxPlots.show = function (opts) {
if (opts!==undefined) {
opts.show=true;
if (opts.reset) {chart.boxPlots.reset()}
} else {opts = {show:true};}
chart.boxPlots.change(opts)};
chart.boxPlots.hide = function (opts) {
if (opts!==undefined) {
opts.show=false;
if (opts.reset) {chart.boxPlots.reset()}
} else {opts = {show:false};}
chart.boxPlots.change(opts)};
/**
* Update the box plot obj values
*/
chart.boxPlots.update = function () {
var cName, cBoxPlot;
for (cName in chart.groupObjs) {
cBoxPlot = chart.groupObjs[cName].boxPlot;
// Get the box width
var objBounds = getObjWidth(bOpts.boxWidth, cName);
var width = (objBounds.right - objBounds.left);
var sMetrics = {}; //temp var for scaled (plottable) metric values
for (var attr in chart.groupObjs[cName].metrics) {
sMetrics[attr] = null;
sMetrics[attr] = chart.yScale(chart.groupObjs[cName].metrics[attr]);
}
// Box
if (cBoxPlot.objs.box) {
cBoxPlot.objs.box
.attr("x", objBounds.left)
.attr('width', width)
.attr("y", sMetrics.quartile3)
.attr("rx", 1)
.attr("ry", 1)
.attr("height", -sMetrics.quartile3 + sMetrics.quartile1)
}
// Lines
var lineBounds = null;
if (bOpts.lineWidth) {
lineBounds = getObjWidth(bOpts.lineWidth, cName)
} else {
lineBounds = objBounds
}
// --Whiskers
if (cBoxPlot.objs.upperWhisker) {
cBoxPlot.objs.upperWhisker.fence
.attr("x1", lineBounds.left)
.attr("x2", lineBounds.right)
.attr('y1', sMetrics.upperInnerFence)
.attr("y2", sMetrics.upperInnerFence);
cBoxPlot.objs.upperWhisker.line
.attr("x1", lineBounds.middle)
.attr("x2", lineBounds.middle)
.attr('y1', sMetrics.quartile3)
.attr("y2", sMetrics.upperInnerFence);
cBoxPlot.objs.lowerWhisker.fence
.attr("x1", lineBounds.left)
.attr("x2", lineBounds.right)
.attr('y1', sMetrics.lowerInnerFence)
.attr("y2", sMetrics.lowerInnerFence);
cBoxPlot.objs.lowerWhisker.line
.attr("x1", lineBounds.middle)
.attr("x2", lineBounds.middle)
.attr('y1', sMetrics.quartile1)
.attr("y2", sMetrics.lowerInnerFence);
}
// --Median
if (cBoxPlot.objs.median) {
cBoxPlot.objs.median.line
.attr("x1", lineBounds.left)
.attr("x2", lineBounds.right)
.attr('y1', sMetrics.median)
.attr("y2", sMetrics.median);
cBoxPlot.objs.median.circle
.attr("cx", lineBounds.middle)
.attr("cy", sMetrics.median)
}
// --Mean
if (cBoxPlot.objs.mean) {
cBoxPlot.objs.mean.line
.attr("x1", lineBounds.left)
.attr("x2", lineBounds.right)
.attr('y1', sMetrics.mean)
.attr("y2", sMetrics.mean);
cBoxPlot.objs.mean.circle
.attr("cx", lineBounds.middle)
.attr("cy", sMetrics.mean);
}
// Outliers
var pt;
if (cBoxPlot.objs.outliers) {
for (pt in cBoxPlot.objs.outliers) {
cBoxPlot.objs.outliers[pt].point
.attr("cx", objBounds.middle+addJitter(bOpts.scatterOutliers,width))
.attr("cy", chart.yScale(cBoxPlot.objs.outliers[pt].value));
}
}
if (cBoxPlot.objs.extremes) {
for (pt in cBoxPlot.objs.extremes) {
cBoxPlot.objs.extremes[pt].point
.attr("cx", objBounds.middle+addJitter(bOpts.scatterOutliers,width))
.attr("cy", chart.yScale(cBoxPlot.objs.extremes[pt].value));
}
}
}
};
/**
* Create the svg elements for the box plot
*/
chart.boxPlots.prepareBoxPlot = function() {
var cName, cBoxPlot;
if (bOpts.colors) {
chart.boxPlots.colorFunct = getColorFunct(bOpts.colors);
} else {
chart.boxPlots.colorFunct = chart.colorFunct
}
if (bOpts.show==false) {return}
for (cName in chart.groupObjs) {
cBoxPlot = chart.groupObjs[cName].boxPlot;
cBoxPlot.objs.g = chart.groupObjs[cName].g.append("g").attr("class", "box-plot");
//Plot Box (default show)
if (bOpts.showBox) {
cBoxPlot.objs.box = cBoxPlot.objs.g.append("rect")
.attr("class", "box")
.style("fill", chart.boxPlots.colorFunct(cName))
.style("stroke", chart.boxPlots.colorFunct(cName));
//A stroke is added to the box with the group color, it is
// hidden by default and can be shown through css with stroke-width
}
//Plot Median (default show)
if (bOpts.showMedian) {
cBoxPlot.objs.median = {line: null, circle: null};
cBoxPlot.objs.median.line = cBoxPlot.objs.g.append("line")
.attr("class", "median");
cBoxPlot.objs.median.circle = cBoxPlot.objs.g.append("circle")
.attr("class", "median")
.attr('r', bOpts.medianCSize)
.style("fill", chart.boxPlots.colorFunct(cName));
}
// Plot Mean (default no plot)
if (bOpts.showMean) {
cBoxPlot.objs.mean = {line: null, circle: null};
cBoxPlot.objs.mean.line = cBoxPlot.objs.g.append("line")
.attr("class", "mean");
cBoxPlot.objs.mean.circle = cBoxPlot.objs.g.append("circle")
.attr("class", "mean")
.attr('r', bOpts.medianCSize)
.style("fill", chart.boxPlots.colorFunct(cName));
}
// Plot Whiskers (default show)
if (bOpts.showWhiskers) {
cBoxPlot.objs.upperWhisker = {fence: null, line: null};
cBoxPlot.objs.lowerWhisker = {fence: null, line: null};
cBoxPlot.objs.upperWhisker.fence = cBoxPlot.objs.g.append("line")
.attr("class", "upper whisker")
.style("stroke", chart.boxPlots.colorFunct(cName));
cBoxPlot.objs.upperWhisker.line = cBoxPlot.objs.g.append("line")
.attr("class", "upper whisker")
.style("stroke", chart.boxPlots.colorFunct(cName));
cBoxPlot.objs.lowerWhisker.fence = cBoxPlot.objs.g.append("line")
.attr("class", "lower whisker")
.style("stroke", chart.boxPlots.colorFunct(cName));
cBoxPlot.objs.lowerWhisker.line = cBoxPlot.objs.g.append("line")
.attr("class", "lower whisker")
.style("stroke", chart.boxPlots.colorFunct(cName));
}
// Plot outliers (default show)
if (bOpts.showOutliers) {
if (!cBoxPlot.objs.outliers) calcAllOutliers();
var pt;
if (cBoxPlot.objs.outliers.length) {
var outDiv = cBoxPlot.objs.g.append("g").attr("class", "boxplot outliers");
for (pt in cBoxPlot.objs.outliers) {
cBoxPlot.objs.outliers[pt].point = outDiv.append("circle")
.attr("class", "outlier")
.attr('r', bOpts.outlierCSize)
.style("fill", chart.boxPlots.colorFunct(cName));
}
}
if (cBoxPlot.objs.extremes.length) {
var extDiv = cBoxPlot.objs.g.append("g").attr("class", "boxplot extremes");
for (pt in cBoxPlot.objs.extremes) {
cBoxPlot.objs.extremes[pt].point = extDiv.append("circle")
.attr("class", "extreme")
.attr('r', bOpts.outlierCSize)
.style("stroke", chart.boxPlots.colorFunct(cName));
}
}
}
}
};
chart.boxPlots.prepareBoxPlot();
d3.select(window).on('resize.' + chart.selector + '.boxPlot', chart.boxPlots.update);
chart.boxPlots.update();
return chart;
};
/**
* Render a notched box on the current chart
* @param options
* @param [options.show=true] Toggle the whole plot on and off
* @param [options.showNotchBox=true] Show the notch box
* @param [options.showLines=false] Show lines at the confidence intervals
* @param [options.boxWidth=35] The width of the widest part of the box
* @param [options.medianWidth=20] The width of the narrowist part of the box
* @param [options.lineWidth=50] The width of the confidence interval lines
* @param [options.notchStyle=null] null=traditional style, 'box' cuts out the whole notch in right angles
* @param [options.colors=chart default] The color mapping for the notch boxes
* @returns {*} The chart object
*/
chart.renderNotchBoxes = function (options) {
chart.notchBoxes = {};
//Defaults
var defaultOptions = {
show:true,
showNotchBox:true,
showLines:false,
boxWidth:35,
medianWidth:20,
lineWidth:50,
notchStyle:null,
colors:null};
chart.notchBoxes.options = shallowCopy(defaultOptions);
for (var option in options) {chart.notchBoxes.options[option] = options[option]}
var nOpts = chart.notchBoxes.options;
//Create notch objects
for (var cName in chart.groupObjs) {
chart.groupObjs[cName].notchBox = {};
chart.groupObjs[cName].notchBox.objs = {};
}
/**
* Makes the svg path string for a notched box
* @param cNotch Current notch box object
* @param notchBounds objBound object
* @returns {string} A string in the proper format for a svg polygon
*/
function makeNotchBox(cNotch, notchBounds) {
var scaledValues = [];
if (nOpts.notchStyle=='box') {
scaledValues = [
[notchBounds.boxLeft, chart.yScale(cNotch.metrics.quartile1)],
[notchBounds.boxLeft, chart.yScale(cNotch.metrics.lowerNotch)],
[notchBounds.medianLeft, chart.yScale(cNotch.metrics.lowerNotch)],
[notchBounds.medianLeft, chart.yScale(cNotch.metrics.median)],
[notchBounds.medianLeft, chart.yScale(cNotch.metrics.upperNotch)],
[notchBounds.boxLeft, chart.yScale(cNotch.metrics.upperNotch)],
[notchBounds.boxLeft, chart.yScale(cNotch.metrics.quartile3)],
[notchBounds.boxRight, chart.yScale(cNotch.metrics.quartile3)],
[notchBounds.boxRight, chart.yScale(cNotch.metrics.upperNotch)],
[notchBounds.medianRight, chart.yScale(cNotch.metrics.upperNotch)],
[notchBounds.medianRight, chart.yScale(cNotch.metrics.median)],
[notchBounds.medianRight, chart.yScale(cNotch.metrics.lowerNotch)],
[notchBounds.boxRight, chart.yScale(cNotch.metrics.lowerNotch)],
[notchBounds.boxRight, chart.yScale(cNotch.metrics.quartile1)]
];
} else {
scaledValues = [
[notchBounds.boxLeft, chart.yScale(cNotch.metrics.quartile1)],
[notchBounds.boxLeft, chart.yScale(cNotch.metrics.lowerNotch)],
[notchBounds.medianLeft, chart.yScale(cNotch.metrics.median)],
[notchBounds.boxLeft, chart.yScale(cNotch.metrics.upperNotch)],
[notchBounds.boxLeft, chart.yScale(cNotch.metrics.quartile3)],
[notchBounds.boxRight, chart.yScale(cNotch.metrics.quartile3)],
[notchBounds.boxRight, chart.yScale(cNotch.metrics.upperNotch)],
[notchBounds.medianRight, chart.yScale(cNotch.metrics.median)],
[notchBounds.boxRight, chart.yScale(cNotch.metrics.lowerNotch)],
[notchBounds.boxRight, chart.yScale(cNotch.metrics.quartile1)]
];
}
return scaledValues.map(function(d) { return [d[0],d[1]].join(","); }).join(" ");
}
/**
* Calculate the confidence intervals
*/
!function calcNotches() {
var cNotch,modifier;
for (var cName in chart.groupObjs) {
cNotch = chart.groupObjs[cName];
modifier = (1.57 * (cNotch.metrics.iqr/Math.sqrt(cNotch.values.length)));
cNotch.metrics.upperNotch = cNotch.metrics.median + modifier;
cNotch.metrics.lowerNotch = cNotch.metrics.median - modifier;
}
}();
/**
* Take a new set of options and redraw the notch boxes
* @param updateOptions
*/
chart.notchBoxes.change = function(updateOptions) {
if (updateOptions) {for (var key in updateOptions) {nOpts[key] = updateOptions[key]}}
for (var cName in chart.groupObjs) {chart.groupObjs[cName].notchBox.objs.g.remove()}
chart.notchBoxes.prepareNotchBoxes();
chart.notchBoxes.update();
};
chart.notchBoxes.reset = function () {chart.notchBoxes.change(defaultOptions)};
chart.notchBoxes.show = function (opts) {
if (opts!==undefined) {
opts.show=true;
if (opts.reset) {chart.notchBoxes.reset()}
} else {opts = {show:true};}
chart.notchBoxes.change(opts)};
chart.notchBoxes.hide = function (opts) {
if (opts!==undefined) {
opts.show=false;
if (opts.reset) {chart.notchBoxes.reset()}
} else {opts = {show:false};}
chart.notchBoxes.change(opts)};
/**
* Update the notch box obj values
*/
chart.notchBoxes.update = function () {
var cName, cGroup;
for (cName in chart.groupObjs) {
cGroup = chart.groupObjs[cName];
// Get the box size
var boxBounds = getObjWidth(nOpts.boxWidth, cName);
var medianBounds = getObjWidth(nOpts.medianWidth, cName);
var notchBounds = {boxLeft:boxBounds.left,
boxRight:boxBounds.right,
middle:boxBounds.middle,
medianLeft:medianBounds.left,
medianRight:medianBounds.right};
// Notch Box
if (cGroup.notchBox.objs.notch) {
cGroup.notchBox.objs.notch
.attr("points",makeNotchBox(cGroup, notchBounds));
}
if (cGroup.notchBox.objs.upperLine) {
var lineBounds = null;
if (nOpts.lineWidth) {
lineBounds = getObjWidth(nOpts.lineWidth, cName)
} else {
lineBounds = objBounds
}
var confidenceLines = {
upper:chart.yScale(cGroup.metrics.upperNotch),
lower:chart.yScale(cGroup.metrics.lowerNotch)
};
cGroup.notchBox.objs.upperLine
.attr("x1", lineBounds.left)
.attr("x2", lineBounds.right)
.attr('y1', confidenceLines.upper)
.attr("y2", confidenceLines.upper);
cGroup.notchBox.objs.lowerLine
.attr("x1", lineBounds.left)
.attr("x2", lineBounds.right)
.attr('y1', confidenceLines.lower)
.attr("y2", confidenceLines.lower);
}
}
};
/**
* Create the svg elements for the notch boxes
*/
chart.notchBoxes.prepareNotchBoxes = function() {
var cName, cNotch;
if (nOpts && nOpts.colors) {
chart.notchBoxes.colorFunct = getColorFunct(nOpts.colors);
} else {
chart.notchBoxes.colorFunct = chart.colorFunct
}
if (nOpts.show==false) {return}
for (cName in chart.groupObjs) {
cNotch = chart.groupObjs[cName].notchBox;
cNotch.objs.g = chart.groupObjs[cName].g.append("g").attr("class", "notch-plot");
// Plot Box (default show)
if (nOpts.showNotchBox) {
cNotch.objs.notch = cNotch.objs.g.append("polygon")
.attr("class", "notch")
.style("fill", chart.notchBoxes.colorFunct(cName))
.style("stroke", chart.notchBoxes.colorFunct(cName));
//A stroke is added to the notch with the group color, it is
// hidden by default and can be shown through css with stroke-width
}
//Plot Confidence Lines (default hide)
if (nOpts.showLines) {
cNotch.objs.upperLine = cNotch.objs.g.append("line")
.attr("class", "upper confidence line")
.style("stroke", chart.notchBoxes.colorFunct(cName));
cNotch.objs.lowerLine = cNotch.objs.g.append("line")
.attr("class", "lower confidence line")
.style("stroke", chart.notchBoxes.colorFunct(cName));
}
}
};
chart.notchBoxes.prepareNotchBoxes();
d3.select(window).on('resize.' + chart.selector + '.notchBox', chart.notchBoxes.update);
chart.notchBoxes.update();
return chart;
};
/**
* Render a raw data in various forms
* @param options
* @param [options.show=true] Toggle the whole plot on and off
* @param [options.showPlot=false] True or false, show points
* @param [options.plotType='none'] Options: no scatter = (false or 'none'); scatter points= (true or [amount=% of width (default=10)]); beeswarm points = ('beeswarm')
* @param [options.pointSize=6] Diameter of the circle in pizels (not the radius)
* @param [options.showLines=['median']] Can equal any of the metrics lines
* @param [options.showbeanLines=false] Options: no lines = false
* @param [options.beanWidth=20] % width
* @param [options.colors=chart default]
* @returns {*} The chart object
*
*/
chart.renderDataPlots = function (options) {
chart.dataPlots = {};
//Defaults
var defaultOptions = {
show:true,
showPlot:false,
plotType:'none',
pointSize:6,
showLines:false,//['median'],
showBeanLines:false,
beanWidth:20,
colors:null};
chart.dataPlots.options = shallowCopy(defaultOptions)
for (var option in options) {chart.dataPlots.options[option] = options[option]}
var dOpts = chart.dataPlots.options;
//Create notch objects
for (var cName in chart.groupObjs) {
chart.groupObjs[cName].dataPlots = {};
chart.groupObjs[cName].dataPlots.objs = {};
}
// The lines don't fit into a group bucket so they live under the dataPlot object
chart.dataPlots.objs = {};
/**
* Take updated options and redraw the data plots
* @param updateOptions
*/
chart.dataPlots.change = function (updateOptions) {
if (updateOptions) {for (var key in updateOptions) {dOpts[key] = updateOptions[key]}}
chart.dataPlots.objs.g.remove();
for (var cName in chart.groupObjs) {chart.groupObjs[cName].dataPlots.objs.g.remove()}
chart.dataPlots.preparePlots();
chart.dataPlots.update()
};
chart.dataPlots.reset = function () {chart.dataPlots.change(defaultOptions)};
chart.dataPlots.show = function (opts) {
if (opts!==undefined) {
opts.show=true;
if (opts.reset) {chart.dataPlots.reset()}
} else {opts = {show:true};}
chart.dataPlots.change(opts)};
chart.dataPlots.hide = function (opts) {
if (opts!==undefined) {
opts.show=false;
if (opts.reset) {chart.dataPlots.reset()}
} else {opts = {show:false};}
chart.dataPlots.change(opts)};
/**
* Update the data plot obj values
*/
chart.dataPlots.update = function () {
var cName, cGroup, cPlot;
// Metrics lines
if (chart.dataPlots.objs.g) {
var halfBand = chart.xScale.rangeBand()/2; // find the middle of each band
for (var cMetric in chart.dataPlots.objs.lines) {
chart.dataPlots.objs.lines[cMetric].line
.x(function (d) {return chart.xScale(d.x)+halfBand});
chart.dataPlots.objs.lines[cMetric].g
.datum(chart.dataPlots.objs.lines[cMetric].values)
.attr('d',chart.dataPlots.objs.lines[cMetric].line);
}
}
for (cName in chart.groupObjs) {
cGroup = chart.groupObjs[cName];
cPlot = cGroup.dataPlots;
if (cPlot.objs.points) {
if (dOpts.plotType=='beeswarm') {
var swarmBounds = getObjWidth(100, cName);
var yPtScale = chart.yScale.copy()
.range([Math.floor(chart.yScale.range()[0]/dOpts.pointSize),0])
.interpolate(d3.interpolateRound)
.domain(chart.yScale.domain());
var maxWidth = Math.floor(chart.xScale.rangeBand()/dOpts.pointSize);
var ptsObj = {};
var cYBucket = null;
// Bucket points
for (var pt = 0; pt<cGroup.values.length; pt++) {
cYBucket = yPtScale(cGroup.values[pt]);
if (ptsObj.hasOwnProperty(cYBucket)!==true) {ptsObj[cYBucket]=[];}
ptsObj[cYBucket].push(cPlot.objs.points.pts[pt]
.attr("cx", swarmBounds.middle)
.attr("cy", yPtScale(cGroup.values[pt])*dOpts.pointSize));
}
// Plot buckets
var rightMax = Math.min(swarmBounds.right-dOpts.pointSize);
for (var row in ptsObj) {
var leftMin = swarmBounds.left+(Math.max((maxWidth - ptsObj[row].length)/2, 0)*dOpts.pointSize);
var col = 0;
for (pt in ptsObj[row]) {
ptsObj[row][pt].attr("cx", Math.min(leftMin+col*dOpts.pointSize,rightMax)+dOpts.pointSize/2);
col++
}
}
} else { // For scatter points and points with no scatter
var plotBounds = null,
scatterWidth=0,
width= 0;
if (dOpts.plotType=='scatter' || typeof dOpts.plotType=='number') {
//Default scatter percentage is 20% of box width
scatterWidth = typeof dOpts.plotType == 'number' ? dOpts.plotType : 20;
}
plotBounds = getObjWidth(scatterWidth, cName);
width = plotBounds.right - plotBounds.left;
for (var pt = 0; pt<cGroup.values.length; pt++) {
cPlot.objs.points.pts[pt]
.attr("cx", plotBounds.middle+addJitter(true,width))
.attr("cy", chart.yScale(cGroup.values[pt]));
}
}
}
if (cPlot.objs.bean) {
var beanBounds = getObjWidth(dOpts.beanWidth, cName);
for (var pt = 0; pt<cGroup.values.length; pt++) {
cPlot.objs.bean.lines[pt]
.attr("x1", beanBounds.left)
.attr("x2", beanBounds.right)
.attr('y1', chart.yScale(cGroup.values[pt]))
.attr("y2", chart.yScale(cGroup.values[pt]));
}
}
}
};
/**
* Create the svg elements for the data plots
*/
chart.dataPlots.preparePlots = function () {
var cName, cPlot;
if (dOpts && dOpts.colors) {
chart.dataPlots.colorFunct = getColorFunct(dOpts.colors);
} else {
chart.dataPlots.colorFunct = chart.colorFunct
}
if (dOpts.show==false) {return}
// Metrics lines
chart.dataPlots.objs.g = chart.objs.g.append("g").attr("class", "metrics-lines");
if (dOpts.showLines && dOpts.showLines.length > 0) {
chart.dataPlots.objs.lines = {};
var cMetric;
for (var line in dOpts.showLines) {
cMetric = dOpts.showLines[line];
chart.dataPlots.objs.lines[cMetric] = {};
chart.dataPlots.objs.lines[cMetric].values = [];
for (var cGroup in chart.groupObjs) {
chart.dataPlots.objs.lines[cMetric].values.push({x:cGroup, y:chart.groupObjs[cGroup].metrics[cMetric]})
}
chart.dataPlots.objs.lines[cMetric].line = d3.svg.line()
.interpolate("cardinal")
.y(function (d) {return chart.yScale(d.y)});
chart.dataPlots.objs.lines[cMetric].g = chart.dataPlots.objs.g.append("path")
.attr("class", "line "+cMetric)
.attr("data-metric", cMetric)
.style("fill", 'none')
.style("stroke", chart.colorFunct(cMetric));
}
}
for (cName in chart.groupObjs) {
cPlot = chart.groupObjs[cName].dataPlots;
cPlot.objs.g = chart.groupObjs[cName].g.append("g").attr("class", "data-plot");
// Points Plot
if (dOpts.showPlot) {
cPlot.objs.points = {g: null, pts: []};
cPlot.objs.points.g = cPlot.objs.g.append("g").attr("class", "points-plot");
for (var pt = 0; pt < chart.groupObjs[cName].values.length; pt++) {
cPlot.objs.points.pts.push(cPlot.objs.points.g.append("circle")
.attr("class", "point")
.attr('r', dOpts.pointSize/2)// Options is diameter, r takes radius so divide by 2
.style("fill", chart.dataPlots.colorFunct(cName)));
}
}
// Bean lines
if (dOpts.showBeanLines) {
cPlot.objs.bean = {g: null, lines: []};
cPlot.objs.bean.g = cPlot.objs.g.append("g").attr("class", "bean-plot");
for (var pt = 0; pt < chart.groupObjs[cName].values.length; pt++) {
cPlot.objs.bean.lines.push(cPlot.objs.bean.g.append("line")
.attr("class", "bean line")
.style("stroke-width", '1')
.style("stroke", chart.dataPlots.colorFunct(cName)));
}
}
}
};
chart.dataPlots.preparePlots();
d3.select(window).on('resize.' + chart.selector + '.dataPlot', chart.dataPlots.update);
chart.dataPlots.update();
return chart;
};
return chart;
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title></title>
<link rel="stylesheet" type="text/css" href="distrochart.css">
<script src="http://d3js.org/d3.v3.min.js" charset="utf-8"></script>
</head>
<body>
<div class="chart-wrapper" id="chart-distro1"></div>
<!--Sorry about all the inline JS. It is a quick way to show what options are available-->
<div class="chart-options">
<p>Show: </p>
<button onclick="chart1.boxPlots.show({reset:true});chart1.violinPlots.hide();chart1.notchBoxes.hide();chart1.dataPlots.change({showPlot:false,showBeanLines:false})">Box Plot</button>
<button onclick="chart1.notchBoxes.show({reset:true});chart1.boxPlots.show({reset:true, showBox:false,showOutliers:true,boxWidth:20,scatterOutliers:true});chart1.violinPlots.hide();chart1.dataPlots.change({showPlot:false,showBeanLines:false})">Notched Box Plot</button>
<button onclick="chart1.violinPlots.show({reset:true, resolution:12});chart1.boxPlots.show({reset:true, showWhiskers:false,showOutliers:false,boxWidth:10,lineWidth:15,colors:['#555']});chart1.notchBoxes.hide();chart1.dataPlots.change({showPlot:false,showBeanLines:false})">Violin Plot</button>
<button onclick="chart1.violinPlots.show({reset:true, width:100, resolution:12});chart1.dataPlots.show({showBeanLines:true,beanWidth:15,showPlot:false,colors:['#555']});chart1.boxPlots.hide();chart1.notchBoxes.hide()">Bean Plot</button>
<button onclick="chart1.dataPlots.show({showPlot:true, plotType:'beeswarm',showBeanLines:false, colors:null});chart1.violinPlots.hide();chart1.notchBoxes.hide();chart1.boxPlots.hide();">Beeswarm Plot</button>
<button onclick="chart1.dataPlots.show({showPlot:true, plotType:40, showBeanLines:false,colors:null});chart1.violinPlots.hide();chart1.notchBoxes.hide();chart1.boxPlots.hide();">Scatter Plot</button>
<button onclick="if(chart1.dataPlots.options.showLines){chart1.dataPlots.change({showLines:false});} else {chart1.dataPlots.change({showLines:['median','quartile1','quartile3']});}">Trend Lines</button>
</div>
<script src="distrochart.js" charset="utf-8"></script>
<script type="text/javascript">
var chart1;
d3.csv('data.csv', function(error, data) {
data.forEach(function (d) {
d.value = +d.value;
});
chart1 = makeDistroChart({
data:data,
xName:'date',
yName:'value',
axisLabels: {xAxis: 'Years', yAxis: 'Values'},
selector:"#chart-distro1",
chartSize:{height:460, width:960},
constrainExtremes:true});
chart1.renderBoxPlot();
chart1.renderDataPlots();
chart1.renderNotchBoxes({showNotchBox:false});
chart1.renderViolinPlot({showViolinPlot:false});
});
</script>
</body>
</html>
@NLSVTN
Copy link

NLSVTN commented Aug 3, 2018

There is an issue with violin plots. Please, see the thread:

https://stackoverflow.com/questions/51679048/violin-plots-are-not-shown-correctly-in-d3-js-app

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment