Skip to content

Instantly share code, notes, and snippets.

@ulyngs
Last active November 10, 2017 17:03
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ulyngs/e1fd5652762df0be26eb4f9b43a364a7 to your computer and use it in GitHub Desktop.
Save ulyngs/e1fd5652762df0be26eb4f9b43a364a7 to your computer and use it in GitHub Desktop.
Grouped horizontal box plot with data points and info on hover

An implementation of a grouped horisontal box plot, with drawing of data points and data shown when hovering over a point.

The plot is easily customizable by adjusting arguments to the main functions drawBoxes, drawPoints, and drawCategoryLabels. See the documentation above each function.

This example (and the mocked up data) is meant to look a bit like plotly's example of a 'Fully Styled Box Plot'.

I started from Matt Brehmer's simple box plot w/ hover and used d3noob's implementation of tooltips on hover.

body {
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
}
.canvas
{
fill: lightblue;
opacity: 0.3;
}
.verticalGrid
{
fill: none;
shape-rendering: crispEdges;
stroke: white;
stroke-width: 1px;
}
.title
{
text-anchor: middle;
font-size: 16px;
font-weight: bold;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
}
.whisker
{
stroke-width: 1px;
}
.median
{
stroke: black;
stroke-width: 1.3px;
}
.box
{
opacity: 0.6;
}
circle
{
stroke: none;
shape-rendering: crispEdges;
}
.categoryLabel
{
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
font-size: 0.75em;
}
.outlier
{
opacity: 1;
}
.point
{
opacity: 0.7;
}
.axis path
{
fill: none;
stroke: black;
stroke-width: 1px;
shape-rendering: crispEdges;
}
.axis line
{
fill: none;
stroke: black;
stroke-width: 1px;
shape-rendering: crispEdges;
}
.axis text
{
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
font-size: 10px;
}
div.tooltip
{
position: absolute;
text-align: center;
padding: 3px;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
font-size: 11px;
background: lightsteelblue;
border: 0px;
border-radius: 8px;
pointer-events: none;
}
playerList goals
1 Carmelo Anthony 4
2 Carmelo Anthony 2
3 Carmelo Anthony 10
4 Carmelo Anthony 3
5 Carmelo Anthony 1
6 Carmelo Anthony 3
7 Carmelo Anthony 8
8 Carmelo Anthony 2
9 Carmelo Anthony 6
10 Carmelo Anthony 7
11 Carmelo Anthony 19
12 Carmelo Anthony 8
13 Carmelo Anthony 5
14 Carmelo Anthony 8
15 Carmelo Anthony 1
16 Carmelo Anthony 8
17 Carmelo Anthony 3
18 Carmelo Anthony 8
19 Carmelo Anthony 2
20 Carmelo Anthony 7
21 Carmelo Anthony 7
22 Carmelo Anthony 1
23 Carmelo Anthony 2
24 Carmelo Anthony 2
25 Carmelo Anthony 8
26 Carmelo Anthony 6
27 Carmelo Anthony 8
28 Carmelo Anthony 7
29 Carmelo Anthony 4
30 Carmelo Anthony 6
31 Dwyane Wade 11
32 Dwyane Wade 7
33 Dwyane Wade 15
34 Dwyane Wade 7
35 Dwyane Wade 3
36 Dwyane Wade 16
37 Dwyane Wade 15
38 Dwyane Wade 14
39 Dwyane Wade 13
40 Dwyane Wade 19
41 Dwyane Wade 8
42 Dwyane Wade 5
43 Dwyane Wade 20
44 Dwyane Wade 3
45 Dwyane Wade 6
46 Dwyane Wade 16
47 Dwyane Wade 7
48 Dwyane Wade 4
49 Dwyane Wade 2
50 Dwyane Wade 16
51 Dwyane Wade 17
52 Dwyane Wade 19
53 Dwyane Wade 15
54 Dwyane Wade 9
55 Dwyane Wade 14
56 Dwyane Wade 2
57 Dwyane Wade 4
58 Dwyane Wade 6
59 Dwyane Wade 12
60 Dwyane Wade 7
61 Deron Williams 0
62 Deron Williams 25
63 Deron Williams 15
64 Deron Williams 14
65 Deron Williams 14
66 Deron Williams 2
67 Deron Williams 23
68 Deron Williams 0
69 Deron Williams 20
70 Deron Williams 21
71 Deron Williams 23
72 Deron Williams 5
73 Deron Williams 22
74 Deron Williams 21
75 Deron Williams 8
76 Deron Williams 0
77 Deron Williams 5
78 Deron Williams 3
79 Deron Williams 24
80 Deron Williams 21
81 Deron Williams 20
82 Deron Williams 0
83 Deron Williams 3
84 Deron Williams 24
85 Deron Williams 24
86 Deron Williams 16
87 Deron Williams 3
88 Deron Williams 2
89 Deron Williams 3
90 Deron Williams 4
91 Brook Lopez 37
92 Brook Lopez 12
93 Brook Lopez 35
94 Brook Lopez 13
95 Brook Lopez 12
96 Brook Lopez 37
97 Brook Lopez 34
98 Brook Lopez 32
99 Brook Lopez 15
100 Brook Lopez 36
101 Brook Lopez 36
102 Brook Lopez 19
103 Brook Lopez 30
104 Brook Lopez 26
105 Brook Lopez 33
106 Brook Lopez 16
107 Brook Lopez 24
108 Brook Lopez 18
109 Brook Lopez 32
110 Brook Lopez 34
111 Brook Lopez 10
112 Brook Lopez 37
113 Brook Lopez 28
114 Brook Lopez 22
115 Brook Lopez 27
116 Brook Lopez 29
117 Brook Lopez 19
118 Brook Lopez 23
119 Brook Lopez 26
120 Brook Lopez 24
121 Damian Lillard 14
122 Damian Lillard 6
123 Damian Lillard 45
124 Damian Lillard 25
125 Damian Lillard 43
126 Damian Lillard 36
127 Damian Lillard 17
128 Damian Lillard 32
129 Damian Lillard 44
130 Damian Lillard 23
131 Damian Lillard 16
132 Damian Lillard 39
133 Damian Lillard 41
134 Damian Lillard 24
135 Damian Lillard 21
136 Damian Lillard 30
137 Damian Lillard 36
138 Damian Lillard 30
139 Damian Lillard 13
140 Damian Lillard 34
141 Damian Lillard 44
142 Damian Lillard 42
143 Damian Lillard 8
144 Damian Lillard 40
145 Damian Lillard 31
146 Damian Lillard 6
147 Damian Lillard 16
148 Damian Lillard 10
149 Damian Lillard 28
150 Damian Lillard 14
151 David West 11
152 David West 11
153 David West 28
154 David West 17
155 David West 28
156 David West 24
157 David West 8
158 David West 27
159 David West 19
160 David West 19
161 David West 16
162 David West 4
163 David West 7
164 David West 14
165 David West 17
166 David West 7
167 David West 12
168 David West 17
169 David West 27
170 David West 1
171 David West 39
172 David West 16
173 David West 21
174 David West 21
175 David West 5
176 David West 29
177 David West 16
178 David West 8
179 David West 4
180 David West 19
181 Blake Griffin 10
182 Blake Griffin 20
183 Blake Griffin 5
184 Blake Griffin 2
185 Blake Griffin 13
186 Blake Griffin 15
187 Blake Griffin 16
188 Blake Griffin 14
189 Blake Griffin 8
190 Blake Griffin 19
191 Blake Griffin 18
192 Blake Griffin 18
193 Blake Griffin 14
194 Blake Griffin 4
195 Blake Griffin 9
196 Blake Griffin 11
197 Blake Griffin 16
198 Blake Griffin 12
199 Blake Griffin 20
200 Blake Griffin 17
201 Blake Griffin 18
202 Blake Griffin 16
203 Blake Griffin 3
204 Blake Griffin 19
205 Blake Griffin 14
206 Blake Griffin 16
207 Blake Griffin 19
208 Blake Griffin 8
209 Blake Griffin 13
210 Blake Griffin 19
211 David Lee 14
212 David Lee 13
213 David Lee 14
214 David Lee 9
215 David Lee 7
216 David Lee 10
217 David Lee 15
218 David Lee 8
219 David Lee 15
220 David Lee 13
221 David Lee 15
222 David Lee 6
223 David Lee 7
224 David Lee 9
225 David Lee 12
226 David Lee 13
227 David Lee 11
228 David Lee 15
229 David Lee 11
230 David Lee 8
231 David Lee 12
232 David Lee 14
233 David Lee 10
234 David Lee 15
235 David Lee 8
236 David Lee 10
237 David Lee 11
238 David Lee 37
239 David Lee 10
240 David Lee 10
241 Demar Derozan 24
242 Demar Derozan 33
243 Demar Derozan 43
244 Demar Derozan 43
245 Demar Derozan 36
246 Demar Derozan 34
247 Demar Derozan 38
248 Demar Derozan 22
249 Demar Derozan 32
250 Demar Derozan 30
251 Demar Derozan 34
252 Demar Derozan 28
253 Demar Derozan 35
254 Demar Derozan 25
255 Demar Derozan 37
256 Demar Derozan 41
257 Demar Derozan 43
258 Demar Derozan 36
259 Demar Derozan 20
260 Demar Derozan 33
261 Demar Derozan 32
262 Demar Derozan 36
263 Demar Derozan 24
264 Demar Derozan 21
265 Demar Derozan 29
266 Demar Derozan 26
267 Demar Derozan 41
268 Demar Derozan 35
269 Demar Derozan 39
270 Demar Derozan 22
//initialize the canvas dimensions
var margin = {top: 10, right: 10, bottom: 10, left: 10},
width = 900 - margin.left - margin.right,
height = 750 - margin.top - margin.bottom,
padding = 30, labelWidth = 120,
titleMargin = 40;
// Define the div for the tooltip
var div = d3.select("body").append("div")
.attr("class", "tooltip")
.style("opacity", 0);
var svg = d3.select("body")
.append("svg")
.attr("width", width)
.attr("height", height);
//initialize the x scale
var xScale = d3.scale.linear()
.range([labelWidth, width - padding]);
//initialize the x axis
var xAxis = d3.svg.axis()
.scale(xScale)
.orient("bottom")
.ticks(10);
// add potential background color to chart
svg.append("rect")
.attr("width", width)
.attr("height", height)
.attr("class", "canvas");
// initialise color palette
var palette = d3.scale.category20();
//*******SET INPUT DATA FILE AND DRAWING SETTINGS HERE***********************'
d3.csv("fakeBasketData.csv", function(error,csv) {
// set the domain of the xScale
xScale.domain([0, 45]);
// set the position of the y axis and append it
var xAxisYPos = height - padding;
svg.append("g")
.attr("class", "axis")
.attr("transform", "translate(0, " + xAxisYPos + ")")
.call(xAxis);
// draw vertical grid lines
svg.selectAll("line.verticalGrid")
.data(xScale.ticks(10))
.enter()
.append("line")
.attr("class", "verticalGrid")
.attr("x1", function(d) { return xScale(d); })
.attr("y1", xAxisYPos)
.attr("x2", function(d) { return xScale(d); })
.attr("y2", titleMargin + padding - 3);
// add a title to the chart
svg.append("text")
.attr("x", (width / 2))
.attr("y", titleMargin)
.attr("class", "title")
.text("Goals Scored in Made-Up Basketball Season")
.style("font-size", "20px");
// set which category we want to group by and get them
var groupingCategory = "playerList";
var categories = d3.nest()
.key(function(d) { return d[groupingCategory] })
.entries(csv);
// calculate how much canvas space we've got available to plot the data from each category
var yCanvasSpaceForEach = (xAxisYPos - titleMargin) / (categories.length + 1)
// iterate over each category and draw what you want on the plot
for (var i = 0; i < categories.length; i++) {
// filter the data by the current category
var dataForCategory = csv.filter(function (d) {
if (d[groupingCategory] == categories[i].key) { return d;}
});
// calculate where to plot on the canvas (draws from top to bottom)
var boxY = yCanvasSpaceForEach * (i + 1) + titleMargin;
// draw box-and-whiskers plot
drawBoxes(svg, dataForCategory, colToPlot = "goals", whiskerHeight = 15, boxHeight = 30, boxY, boxNumber = i);
// draw data points
drawPoints(svg, dataForCategory, colToPlot = "goals", colToHover = "playerList", pointSize = 3.5,
outlierSize = 5, boxY, yDisplacement = 25, jitterAmount = 3, categoryIndex = i, hoverX = -5, hoverY = -10);
// draw labels
drawCategoryLabels(svg, label = categories[i].key, fontsize = 13, xPlacement = 5, boxY, yDisplacement = 4);
}
});
//******************HELPER FUNCTIONS BELOW***********************
// function to calculate statistics summary
function calcBoxStats(data){
// initialise stats object
var dataStats = {
outliers: [],
minVal: Infinity,
lowerWhisker: Infinity,
q1Val: Infinity,
medianVal: 0,
q3Val: -Infinity,
iqr: 0,
upperWhisker: -Infinity,
maxVal: -Infinity
};
// sort the data ascending
data = data.sort(d3.ascending);
//calculate statistics
dataStats.minVal = data[0],
dataStats.q1Val = d3.quantile(data, .25),
dataStats.medianVal = d3.quantile(data, .5),
dataStats.q3Val = d3.quantile(data, .75),
dataStats.iqr = dataStats.q3Val - dataStats.q1Val,
dataStats.maxVal = data[data.length - 1];
var index = 0;
//search for the lower whisker, the minimum value within q1Val - 1.5*iqr
while (index < data.length && dataStats.lowerWhisker == Infinity) {
if (data[index] >= (dataStats.q1Val - 1.5*dataStats.iqr))
dataStats.lowerWhisker = data[index];
else
dataStats.outliers.push(data[index]);
index++;
}
index = data.length-1; // reset index to end of array
//search for the upper whisker, the maximum value within q1Val + 1.5*iqr
while (index >= 0 && dataStats.upperWhisker == -Infinity) {
if (data[index] <= (dataStats.q3Val + 1.5 * dataStats.iqr))
dataStats.upperWhisker = data[index];
else
dataStats.outliers.push(data[index]);
index--;
}
return dataStats;
}
/******function to draw box-and-whiskers plot*******
* arguments:
* svg: the svg to plot on
* csv: csv with the data to plot
* colToPlot: name of the column (as a string) containing the data to plot
* whiskerHeight: the length of whiskers you want
* boxHeight: the height of the interquartile range box you want
* boxY: the y-coordinate around which the boxes should be centered
* categoryIndex: the index of the current category of data being plotted
*/
function drawBoxes(svg, csv, colToPlot, whiskerHeight, boxHeight, boxY, categoryIndex) {
// make an array of the data to plot
var data = csv.map(function(d) {
return +d[colToPlot]; // coerce to a number
});
// get statistics for this data
boxStats = calcBoxStats(data);
//draw vertical line for lowerWhisker
svg.append("line")
.attr("class", "whisker")
.attr("x1", xScale(boxStats.lowerWhisker))
.attr("x2", xScale(boxStats.lowerWhisker))
.attr("stroke", "black")
.attr("y1", boxY - (whiskerHeight/2))
.attr("y2", boxY + (whiskerHeight/2));
//draw vertical line for upperWhisker
svg.append("line")
.attr("class", "whisker")
.attr("x1", xScale(boxStats.upperWhisker))
.attr("x2", xScale(boxStats.upperWhisker))
.attr("stroke", "black")
.attr("y1", boxY - (whiskerHeight/2))
.attr("y2", boxY + (whiskerHeight/2));
//draw horizontal line from lowerWhisker to 1st quartile
svg.append("line")
.attr("class", "whisker")
.attr("x1", xScale(boxStats.lowerWhisker))
.attr("x2", xScale(boxStats.q1Val))
.attr("stroke", "black")
.attr("y1", boxY)
.attr("y2", boxY);
//draw rect for iqr
svg.append("rect")
.attr("class", "box")
.attr("stroke", "black")
.attr("fill", palette(categoryIndex)) // sets new color for each box
.attr("x", xScale(boxStats.q1Val))
.attr("y", boxY - (boxHeight/2))
.attr("width", xScale(boxStats.q3Val) - xScale(boxStats.q1Val))
.attr("height", boxHeight);
//draw horizontal line from 3rd quartile to upperWhisker
svg.append("line")
.attr("class", "whisker")
.attr("x1", xScale(boxStats.q3Val))
.attr("x2", xScale(boxStats.upperWhisker))
.attr("stroke", "black")
.attr("y1", boxY)
.attr("y2", boxY);
//draw vertical line at median
svg.append("line")
.attr("class", "median")
.attr("x1", xScale(boxStats.medianVal))
.attr("x2", xScale(boxStats.medianVal))
.attr("y1", boxY - (boxHeight/2))
.attr("y2", boxY + (boxHeight/2));
}
/* ***function to draw the data points***
* arguments:
* svg: the svg to plot on
* csv: csv with data to plot
* colToPlot: name of the column (as a string) containing the data to plot
* colToHover: the name of the column (as a string) from which data should be shown on hover
* pointSize: the size of points you want
* boxY: the y-coordinate around which the points should be centered
* yDisplacement: any desired displacement up or down relative to the center
* jitterAmount: the amount of jitter you want
* categoryIndex: the index of the current category of data being plotted
* hoverX: where, relative to the datapoint, should the hover text be shown horisontally?
* hoverY: where, relative to the datapoint, should the hover text be shown vertically?
*/
function drawPoints(svg, csv, colToPlot, colToHover, pointSize, outlierSize, boxY,
yDisplacement, jitterAmount, categoryIndex, hoverX, hoverY) {
boxY = boxY + yDisplacement;
function random_jitter(boxY) {
if (Math.round(Math.random() * 1) == 0)
var seed = -jitterAmount;
else
var seed = jitterAmount;
return boxY + Math.floor((Math.random() * seed) + 1);
}
// make an array of the data we want to plot
var data = csv.map(function(d) {
return +d[colToPlot];
});
// get statistics
boxStats = calcBoxStats(data);
// make a grouping for each data point
var dataPoints = d3.select("svg")
.selectAll(".dataPoints" + categoryIndex) //select only data points drawn in the current iteration
.data(csv)
.enter()
.append("g")
.attr("class", "dataPoints" + categoryIndex)
.attr("transform", function(d){
return "translate(" + xScale(d[colToPlot]) + "," + random_jitter(boxY) + ")";
})
// show app name when hovering over a data point
.on("mouseover", function(d){
div.transition()
.duration(200)
.style("opacity", .9);
div.html(d[colToHover] + ": " + d[colToPlot])
.style("left", (d3.event.pageX) + "px")
.style("top", (d3.event.pageY - 28) + "px");
console.log(d[colToHover].length);
})
// remove text when we move the mouse away
.on("mouseout", function(d) {
div.transition()
.duration(500)
.style("opacity", 0);
});
// draw the data points as circles
dataPoints
.append("circle")
.attr("r", function(d) {
if (d[colToPlot] < boxStats.lowerWhisker || d[colToPlot] > boxStats.upperWhisker)
return outlierSize;
else
return pointSize;
})
.attr("class", function(d) {
if (d[colToPlot] < boxStats.lowerWhisker || d[colToPlot] > boxStats.upperWhisker)
return "outlier";
else
return "point";
})
.attr("fill", palette(categoryIndex));
}
/* ***function to draw the category labels***
* arguments:
* svg: the svg to plot on
* category: string with the category label to plot
* xDisplacement: where on the horisontal axis should the label be shown?
* boxY: the y coordinate where the data for the category has been plotted
* yDisplacement: any desired displacement up or down of the text
*/
function drawCategoryLabels(svg, label, fontsize, xPlacement, boxY, yDisplacement) {
d3.select("svg")
.append("text")
.attr("class", "categoryLabel")
.text(label)
.style("font-size", fontsize)
.style("font-weight", 400)
.attr("x", xPlacement)
.attr("y", boxY + yDisplacement)
}
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<link rel= "stylesheet" type="text/css" href="boxstyle.css"/>
<title>Grouped Horizontal Box Plot w/ Data Points and Point Hover</title>
</head>
<body>
<script type="text/javascript" src="http://d3js.org/d3.v3.min.js"></script>
<script type="text/javascript" src="groupedBoxPlot.js"> </script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment