Skip to content

Instantly share code, notes, and snippets.

@asielen
Last active March 1, 2023 09:59
Show Gist options
  • Star 8 You must be signed in to star a gist
  • Fork 4 You must be signed in to fork a gist
  • Save asielen/92929960988a8935d907e39e60ea8417 to your computer and use it in GitHub Desktop.
Save asielen/92929960988a8935d907e39e60ea8417 to your computer and use it in GitHub Desktop.
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.

See this live on bl.ocks.org here.

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)

Updated in V3:

  • Support for clamping the ViolinPlot or forcing it to extend beyond the normal range to create a closed Violin
  • New option to adjust the number of y axis ticks
  • Now uses Kernel Density Estimation intstead of histogram interpolation for more accurate violin plots.

Previous version: Reusable Violin + Box Plot V2

date value
2000 208.4968974
2000 160.5328879
2002 292.3321976
1998 95.07969441
2001 251.6346499
1996 4.723143097
1997 221.3608926
2002 257.5135771
1999 256.6401961
1998 20.19655313
2000 280.5287882
2002 195.5122557
1998 177.9101782
1998 35.8381779
1997 157.4465176
1999 134.3793597
1997 150.604782
1996 -163.8499657
1997 137.4253423
2001 142.7192938
1999 180.7018929
1998 115.9725529
1999 209.3638088
1998 67.84781771
1996 175.231925
2002 51.47799284
1999 188.9962324
1996 -48.35468425
1997 169.423597
1998 -53.22055537
1997 292.1632604
2001 136.4384768
2002 321.5455618
1999 53.06249276
2002 340.8281495
2002 130.9466336
2002 286.8816131
2000 176.4176712
1998 191.6883802
2001 150.0037128
2000 197.7215175
2001 305.2651151
1999 210.168763
2001 115.5839981
1998 175.7373095
1999 116.9958817
1998 154.8568107
1996 14.6993532
2001 198.5466972
1999 74.15721631
1996 114.734763
1999 102.2094761
1998 177.7200953
2002 135.5771092
2002 262.2642028
2001 146.5137898
1998 157.1558524
2002 100.7217744
1999 215.9330216
1998 77.73977658
2002 307.4118429
2002 183.3339337
1999 197.9264315
1996 17.60508917
1996 210.2650095
1996 -61.72121173
1996 114.4151786
2002 137.0691326
1996 196.452651
1996 -93.70487623
1996 94.04043151
2002 243.9383793
1998 185.4923709
2001 86.83137214
1997 189.7194604
1999 107.6012989
1997 111.3635375
1996 -18.48801027
2001 284.114423
1998 25.03677561
2001 194.6109073
2002 222.8485575
2001 269.0836685
1997 42.56959913
2002 263.6498678
2002 141.9210707
1996 108.4558658
2000 136.6209948
1998 172.4753343
1999 147.918509
1998 153.3322857
2000 165.9668168
1999 177.2947913
2002 -74.31511032
2002 335.3878377
1999 87.78180299
2001 256.9765118
2000 156.3968699
1998 187.2355674
1996 26.95490135
1996 205.3224574
1996 -146.8977273
1997 111.4247665
1997 39.31960853
1998 165.4031941
2001 76.54635096
2000 211.4411524
2002 85.38760996
2002 258.6304837
1999 101.40771
2002 319.3656086
1999 48.89019215
1996 185.5018042
2002 44.27040391
1998 163.9139191
1999 64.91277185
1999 214.75898
2000 95.95428713
1997 152.1584732
2001 105.5137981
2000 204.940937
2002 168.8783255
1997 109.6414378
1996 8.294135496
1996 170.1018831
2001 133.4457303
1997 154.7432792
2001 115.9420248
1997 161.6765493
2001 318.3716388
2000 185.0529758
1996 -25.0084555
1998 179.3206217
2002 23.77085763
1996 109.5537878
2001 104.5309686
1998 188.4592993
1997 -42.71530849
1998 191.2920462
1999 133.7938658
1998 159.0451771
2002 178.4659497
1998 236.824034
2001 65.69920953
1997 176.8594544
2002 224.9232276
2001 353.1720826
1996 -42.54134484
2002 352.5103937
2002 100.4976596
2001 262.7544883
1998 78.31221195
1996 161.2249696
1998 77.25946692
2002 320.1315855
2000 147.2817322
2002 257.4599337
1997 69.08830619
1998 146.0831955
2002 113.8032144
1999 205.7691001
2001 117.1322359
1997 130.8596499
1996 1.95131609
2001 262.9490431
2001 34.79418313
1997 101.7745406
1999 49.77164944
2001 200.7904755
2002 161.5282583
2001 216.4782181
1996 -33.33688556
2001 235.903581
1998 77.52683993
1996 109.03816
1998 46.23212288
2002 334.8055355
2002 -28.40462897
2001 259.6404954
2000 146.3087239
2002 377.0370575
1997 26.75431767
2002 263.8179041
1998 -16.58595091
1999 225.6157298
2002 -42.35546988
2000 234.5228736
1996 -38.9393706
1999 211.1955424
1998 37.78872187
1998 186.3913279
2001 162.9298056
2001 326.0401303
2002 244.4557295
1996 121.3493094
1996 -4.908452899
2000 289.8393967
2002 231.050691
2000 185.6270916
2001 217.0400562
2000 233.1733188
1997 -108.585529
1997 132.1325814
2000 168.6266924
1997 192.0853546
1998 46.22287178
1999 192.0663673
1997 -76.42243079
1996 -166.2188619
1997 50.57489598
1997 161.6687837
1996 11.57283366
1996 176.3964678
2002 67.80298236
2002 225.2487353
2002 132.5723879
2000 276.3019917
1999 124.5530979
2001 301.9152608
2002 85.22160659
2002 291.9140151
2002 122.4231766
1997 213.9817405
2000 164.1858424
1996 110.8755204
2001 -12.51757909
2002 364.7130522
1997 25.74815884
2002 362.1798034
1997 19.35952907
1999 171.6071014
1999 124.2586256
2000 242.8487277
2001 149.2189275
1997 153.4503189
1997 30.03059153
1997 140.9275416
1996 -51.29477103
2000 250.9379606
2002 158.3533996
1998 130.182317
2001 138.7092058
2002 253.3304494
2002 144.9757234
1996 178.5478547
2000 72.20396078
1996 553.9499109
2002 219.5272559
1998 135.3017077
1996 2.750346155
1999 164.5810382
1996 29.28765195
1998 171.8155041
1996 -62.47847974
1997 151.5809857
2002 134.6323019
1999 212.9892487
2002 89.75102376
2000 283.2522823
1999 89.39028149
2001 278.4404473
1996 -109.3304066
1999 229.1511074
1999 62.34497978
2000 85.230187
1999 100.4950058
1997 200.2309017
1999 76.72850604
2000 229.9301867
1998 72.15344724
1998 195.0161825
1999 94.87059541
1997 157.0910643
2001 65.01399632
2001 297.1591558
1998 20.07084747
1999 233.4660872
2001 216.3095206
1997 170.52204
1999 78.50367791
2000 239.9552241
1997 2.147629172
2002 379.3151119
1998 51.57920743
2000 261.4090462
1998 43.44942227
1997 132.7226702
2000 175.8934445
2000 277.2232739
2002 184.9889427
1996 120.8580358
1997 191.6720426
2001 187.6245982
2002 179.1492148
1999 157.2360451
2001 80.04527985
1997 212.3687904
1998 24.00284469
1996 114.4805217
1997 -4.064305421
1997 226.8353268
2002 227.1109639
2000 279.0223834
1996 21.41081879
1997 143.8646094
2001 158.1113357
1998 184.2694171
1998 59.4411768
1996 150.9424472
2002 227.4581954
2001 293.3287564
2000 155.2869436
1996 181.2817844
1999 118.3508146
2002 290.9272223
1998 -25.95669287
2000 261.577609
2001 137.9238059
1996 104.2415804
2001 110.8406592
1998 214.1830759
2000 182.1599734
1997 -80.82039329
1999 80.93972737
2000 233.3097023
1996 -148.9825013
1996 102.8203318
1996 17.94859818
2000 232.4654949
1999 127.3053161
1998 189.5161067
1997 52.03000927
2000 266.2037164
2001 19.61896068
2002 310.2054732
1998 95.51888317
2002 565.7785986
1997 49.75458286
1997 165.5522385
1997 46.2049385
1998 178.0625039
1996 17.27953926
1997 261.8950031
2001 143.8183958
2000 250.1691319
1996 25.95785178
2000 179.6837376
1996 -43.26549148
1998 151.4800229
1996 -111.4736412
2001 233.9101271
2001 164.7412837
1996 208.0000028
1996 20.66494709
1997 235.1549474
1998 35.52670759
2000 228.8558584
1999 67.91927028
1996 514.1211521
2002 137.5345718
1997 137.2434424
1998 18.38698421
2001 188.2074573
1998 -27.98708345
1996 196.0813888
2000 156.5011947
1999 164.2303054
2001 155.72949
1996 188.3434843
2001 172.8608446
1996 108.7538702
2002 158.4953604
1997 295.1204317
2000 202.7568375
1999 192.4999169
1998 70.87167826
2002 434.4384007
2002 14.89312532
2000 282.049065
1997 33.9431407
1999 226.8977153
1997 26.20327452
1996 118.5680419
2002 116.6038789
2002 701.1076239
1998 18.20232892
1996 97.88270558
1997 -57.92522621
2000 255.764516
2000 54.52055825
1999 206.9950256
2002 222.5568434
1999 209.7686251
1996 10.84328606
1998 170.9119633
2001 178.7836109
2001 404.1838318
2000 75.59836591
2002 335.7867388
2000 188.021937
1998 35.05881498
1997 60.93804001
1996 105.3636852
1998 45.08619354
1999 182.6039742
1998 41.82386356
1996 126.2237861
2000 106.6725667
1996 167.2021452
2002 88.59645944
2002 334.4911434
1998 124.9516826
2002 308.8227928
2001 98.87445255
1998 127.9427486
2002 139.3041594
1997 144.5111193
1998 146.7772939
1996 111.0311866
2000 143.0060368
1997 266.3802546
1996 -56.52883643
1997 165.2809079
1999 76.76795913
2002 357.0434218
2000 39.42975856
2001 200.3437131
1996 -7.375059038
2002 402.6828173
1999 138.1697845
1997 133.987686
1999 133.9946493
2001 419.2625726
1996 -54.20342289
1997 177.5902054
1996 -5.268905046
1996 110.6727969
2001 76.98892296
2000 220.6703596
2000 84.80589751
2002 -133.7878417
2001 159.1013487
1996 101.4781021
2002 221.1297277
1997 160.6555138
1999 100.9936022
1997 126.2748973
2000 66.52701701
1996 110.6464315
2002 36.15946532
1999 226.3014108
1997 21.72055667
2000 167.935579
1998 20.81132199
1999 227.8543829
1996 25.76979155
1997 244.1586111
2000 177.1136973
1999 221.050831
1999 110.4931264
2000 223.5116122
1999 122.060817
1997 148.775981
2001 135.7563109
1997 208.8947212
2001 131.5311888
1998 179.4150518
1997 27.32787774
1997 231.3493247
1997 37.53502314
1996 118.1465839
2000 31.11532162
2002 267.8910308
2001 102.2021658
1997 193.4957639
1999 63.44883985
2000 261.3125672
2000 33.74883377
1999 195.3846233
2001 83.74423595
2002 484.2443322
1996 -38.29618771
1997 147.840383
1996 1.485343235
1998 165.556157
1999 144.0741205
2001 403.9901334
1996 2.132530501
2001 350.3402704
1997 -25.94177964
2000 240.3780517
1998 41.14205171
2002 35.363135
2002 113.1600897
2000 168.8637489
1996 -25.96838117
1997 125.7448262
1996 -133.4504018
1999 165.2567402
1997 39.80787742
/*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 3.0
*/
/**
* 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.yTicks = 1] 1 = default ticks. 2 = double, 0.5 = half
* @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,
yTicks: 1,
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.yAxis.ticks(chart.objs.yAxis.ticks()*chart.settings.yTicks);
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 the yDomain if the Violin plot clamp is set to -1 meaning it will extend the violins to make nice points
if (chart.violinPlots && chart.violinPlots.options.show == true && chart.violinPlots.options._yDomainVP != null) {
chart.yScale.domain(chart.violinPlots.options._yDomainVP).nice().clamp(true);
} else {
chart.yScale.domain(chart.range).nice().clamp(true);
}
//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=100 default]
* @param [options.bandwidth=10 default] May need higher bandwidth for larger data sets
* @param [options.width=50] The max percent of the group rangeBand that the violin can be
* @param [options.interpolation=''] How to render the violin
* @param [options.clamp=0 default]
* 0 = keep data within chart min and max, clamp once data = 0. May extend beyond data set min and max
* 1 = clamp at min and max of data set. Possibly no tails
* -1 = extend chart axis to make room for data to interpolate to 0. May extend axis and data set min and max
* @param [options.colors=chart default] The color mapping for the violin plot
* @returns {*} The chart object
*/
chart.renderViolinPlot = function (options) {
chart.violinPlots = {};
var defaultOptions = {
show: true,
showViolinPlot: true,
resolution: 100,
bandwidth: 20,
width: 50,
interpolation: 'cardinal',
clamp: 1,
colors: chart.colorFunct,
_yDomainVP: null // If the Violin plot is set to close all violin plots, it may need to extend the domain, that extended domain is stored here
};
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 = {};
}
/**
* 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;
// Build the violins sideways, so use the yScale for the xScale and make a new yScale
var xVScale = chart.yScale.copy();
// Create the Kernel Density Estimator Function
cViolinPlot.kde = kernelDensityEstimator(eKernel(vOpts.bandwidth), xVScale.ticks(vOpts.resolution));
cViolinPlot.kdedata = cViolinPlot.kde(chart.groupObjs[cName].values);
var interpolateMax = chart.groupObjs[cName].metrics.max,
interpolateMin = chart.groupObjs[cName].metrics.min;
if (vOpts.clamp == 0 || vOpts.clamp == -1) { //
// When clamp is 0, calculate the min and max that is needed to bring the violin plot to a point
// interpolateMax = the Minimum value greater than the max where y = 0
interpolateMax = d3.min(cViolinPlot.kdedata.filter(function (d) {
return (d.x > chart.groupObjs[cName].metrics.max && d.y == 0)
}), function (d) {
return d.x;
});
// interpolateMin = the Maximum value less than the min where y = 0
interpolateMin = d3.max(cViolinPlot.kdedata.filter(function (d) {
return (d.x < chart.groupObjs[cName].metrics.min && d.y == 0)
}), function (d) {
return d.x;
});
// If clamp is -1 we need to extend the axises so that the violins come to a point
if (vOpts.clamp == -1) {
kdeTester = eKernelTest(eKernel(vOpts.bandwidth), chart.groupObjs[cName].values);
if (!interpolateMax) {
var interMaxY = kdeTester(chart.groupObjs[cName].metrics.max);
var interMaxX = chart.groupObjs[cName].metrics.max;
var count = 25; // Arbitrary limit to make sure we don't get an infinite loop
while (count > 0 && interMaxY != 0) {
interMaxY = kdeTester(interMaxX);
interMaxX += 1;
count -= 1;
}
interpolateMax = interMaxX;
}
if (!interpolateMin) {
var interMinY = kdeTester(chart.groupObjs[cName].metrics.min);
var interMinX = chart.groupObjs[cName].metrics.min;
var count = 25; // Arbitrary limit to make sure we don't get an infinite loop
while (count > 0 && interMinY != 0) {
interMinY = kdeTester(interMinX);
interMinX -= 1;
count -= 1;
}
interpolateMin = interMinX;
}
}
// Check to see if the new values are outside the existing chart range
// If they are assign them to the master _yDomainVP
if (!vOpts._yDomainVP) vOpts._yDomainVP = chart.range.slice(0);
if (interpolateMin && interpolateMin < vOpts._yDomainVP[0]) {
vOpts._yDomainVP[0] = interpolateMin;
}
if (interpolateMax && interpolateMax > vOpts._yDomainVP[1]) {
vOpts._yDomainVP[1] = interpolateMax;
}
}
if (vOpts.showViolinPlot) {
chart.update();
xVScale = chart.yScale.copy();
// Need to recalculate the KDE because the xVScale changed
cViolinPlot.kde = kernelDensityEstimator(eKernel(vOpts.bandwidth), xVScale.ticks(vOpts.resolution));
cViolinPlot.kdedata = cViolinPlot.kde(chart.groupObjs[cName].values);
}
cViolinPlot.kdedata = cViolinPlot.kdedata
.filter(function (d) {
return (!interpolateMin || d.x >= interpolateMin)
})
.filter(function (d) {
return (!interpolateMax || d.x <= interpolateMax)
});
}
for (cName in chart.groupObjs) {
cViolinPlot = chart.groupObjs[cName].violin;
// Get the violin width
var objBounds = getObjWidth(vOpts.width, cName);
var width = (objBounds.right - objBounds.left) / 2;
var yVScale = d3.scale.linear()
.range([width, 0])
.domain([0, d3.max(cViolinPlot.kdedata, 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.kdedata)
.attr("d", area);
cViolinPlot.objs.left.line
.datum(cViolinPlot.kdedata)
.attr("d", line);
cViolinPlot.objs.right.area
.datum(cViolinPlot.kdedata)
.attr("d", area);
cViolinPlot.objs.right.line
.datum(cViolinPlot.kdedata)
.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));
}
}
};
function kernelDensityEstimator(kernel, x) {
return function (sample) {
return x.map(function (x) {
return {x:x, y:d3.mean(sample, function (v) {return kernel(x - v);})};
});
};
}
function eKernel(scale) {
return function (u) {
return Math.abs(u /= scale) <= 1 ? .75 * (1 - u * u) / scale : 0;
};
}
// Used to find the roots for adjusting violin axis
// Given an array, find the value for a single point, even if it is not in the domain
function eKernelTest(kernel, array) {
return function (testX) {
return d3.mean(array, function (v) {return kernel(testX - v);})
}
}
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.violinPlots.hide();chart1.boxPlots.show({reset:true});chart1.notchBoxes.hide();chart1.dataPlots.change({showPlot:false,showBeanLines:false})">Box Plot</button>
<button onclick="chart1.violinPlots.hide();chart1.notchBoxes.show({reset:true});chart1.boxPlots.show({reset:true, showBox:false,showOutliers:true,boxWidth:20,scatterOutliers:true});chart1.dataPlots.change({showPlot:false,showBeanLines:false})">Notched Box Plot</button>
<button onclick="chart1.violinPlots.show({reset:true,clamp:0});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 Unbound</button>
<button onclick="chart1.violinPlots.show({reset:true,clamp:1});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 Clamp to Data</button>
<button onclick="chart1.violinPlots.show({reset:true, width:75, clamp:0, resolution:30, bandwidth:50});chart1.dataPlots.show({showBeanLines:true,beanWidth:15,showPlot:false,colors:['#555']});chart1.boxPlots.hide();chart1.notchBoxes.hide()">Bean Plot</button>
<button onclick="chart1.violinPlots.hide();chart1.dataPlots.show({showPlot:true, plotType:'beeswarm',showBeanLines:false, colors:null});chart1.notchBoxes.hide();chart1.boxPlots.hide();">Beeswarm Plot</button>
<button onclick="chart1.violinPlots.hide();chart1.dataPlots.show({showPlot:true, plotType:40, showBeanLines:false,colors:null});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: null, 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment