Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 80 additions & 0 deletions auto_tests/tests/boxplot.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/**
* @fileoverview Tests for boxplot option.
*
* @author vojtech.horky@gmail.com (Vojtech Horky)
*/
import Dygraph from '../../src/dygraph';
import * as utils from '../../src/dygraph-utils';
import Proxy from './Proxy';
import CanvasAssertions from './CanvasAssertions';

describe("boxplot", function() {

cleanupAfterEach();
useProxyCanvas(utils, Proxy);

//Here we assume the values are at 1,2,3,... (without extra gaps)
var assertWhisker = function(graph, x_center, y_box, y_whisker, predicate) {
CanvasAssertions.assertLineDrawn(graph.hidden_ctx_,
graph.toDomCoords(x_center - 1.0/6.0, y_whisker),
graph.toDomCoords(x_center + 1.0/6.0, y_whisker), predicate);
CanvasAssertions.assertLineDrawn(graph.hidden_ctx_,
graph.toDomCoords(x_center, y_whisker),
graph.toDomCoords(x_center, y_box), predicate);
};

var assertBox = function(graph, x_center, y_low, y_mid, y_high, predicate) {
CanvasAssertions.assertLineDrawn(graph.hidden_ctx_,
graph.toDomCoords(x_center - 1.0/3.0, y_mid),
graph.toDomCoords(x_center + 1.0/3.0, y_mid), predicate);
// Rectangle is not captured by the context.
//CanvasAssertions.assertLineDrawn(graph.hidden_ctx_,
// graph.toDomCoords(x_center - 1.0/3.0, y_low),
// graph.toDomCoords(x_center + 1.0/3.0, y_low), predicate);
//CanvasAssertions.assertLineDrawn(graph.hidden_ctx_,
// graph.toDomCoords(x_center - 1.0/3.0, y_high),
// graph.toDomCoords(x_center + 1.0/3.0, y_high), predicate);
//CanvasAssertions.assertLineDrawn(graph.hidden_ctx_,
// graph.toDomCoords(x_center - 1.0/3.0, y_low),
// graph.toDomCoords(x_center - 1.0/3.0, y_high), predicate);
//CanvasAssertions.assertLineDrawn(graph.hidden_ctx_,
// graph.toDomCoords(x_center + 1.0/3.0, y_low),
// graph.toDomCoords(x_center + 1.0/3.0, y_high), predicate);
}


it('testSimpleBoxplot', function() {
var opts = {
width: 400,
height: 320,
boxplot: true,
colors: [ "#ff0000" ],
xRangePad: 200,
labels: [ "X", "Y" ]
};
var data = [
[ 1, [ 25, 5, 17, 33, 48] ],
[ 2, [ 20, 3, 19, 22, 31] ]
];

var graph = document.getElementById("graph");
var g = new Dygraph(graph, data, opts);

// Test number of lines drawn. Here only the whiskers, median line and
// the connecting line are counted.
assert.equal(11, CanvasAssertions.numLinesDrawn(g.hidden_ctx_, "#ff0000"));

var props = { strokeStyle: "#ff0000" };

// Assert the (first) box is drawn.
assertBox(g, 1, 17, 25, 33, props);
assertWhisker(g, 1, 17, 5, props);
assertWhisker(g, 1, 33, 48, props);

// Assert the second box.
assertBox(g, 2, 19, 20, 22, props);
assertWhisker(g, 2, 19, 3, props);
assertWhisker(g, 2, 22, 31, props);
});

});
17 changes: 17 additions & 0 deletions gallery/boxplot.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
Gallery.register(
'boxplot',
{
name: 'Boxplot Demo',
title: 'Boxplot Demo',
setup: function(parent) {
parent.innerHTML = "<div id='boxplot_div' style='width: 600px; height: 300px;'></div><br/>";
},
run: function() {
var g = new Dygraph(document.getElementById("boxplot_div"), dataBoxplot,
{
boxplot: true,
xRangePad: 100,
strokeWidth: 0.0
});
}
});
9 changes: 9 additions & 0 deletions gallery/data.js
Original file line number Diff line number Diff line change
Expand Up @@ -2683,3 +2683,12 @@ var stockData = function() {
"2009-09-15,9280.67;9712.28;9829.87,4297.2232125907;4497.07133894216;4551.51896800004\n" +
"2009-10-15,9487.67;9712.73;10092.2,4388.84340147194;4492.9525342659;4668.48924723722\n";
};

var dataBoxplot = function() {
return "Index,Alpha,Bravo\n" +
"1,68;51;61;74;134,17;12;19\n" +
"2,76;20;31;106;137,50;23;35;47;56;91\n" +
"3,35;4;8;45;90;136,21;10;17;20;23;35\n" +
"4,21;10;17;20;23;35,32;4;8;90;136\n"
;
};
1 change: 1 addition & 0 deletions gallery/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
<script src="synchronize.js"></script>
<script src="range-selector.js"></script>
<script src="resize.js"></script>
<script src="boxplot.js"></script>
<script src="stock.js"></script>
<script src="styled-chart-labels.js"></script>
<script src="temperature-sf-ny.js"></script>
Expand Down
119 changes: 119 additions & 0 deletions src/datahandler/boxplot.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
/**
* @license
* Copyright 2016 Vojtech Horky (vojtech.horky@gmail.com)
* MIT-licensed (http://opensource.org/licenses/MIT)
*/

/**
* @fileoverview DataHandler implementation for the "boxplot" data formats.
*
* @author Vojtech Horky (vojtech.horky@gmail.com)
*/

import DygraphDataHandler from './datahandler';
import DygraphLayout from '../dygraph-layout';

"use strict";

var BoxplotHandler = function() {
DygraphDataHandler.call(this);
};

BoxplotHandler.prototype = new DygraphDataHandler();

/** @inheritDoc */
BoxplotHandler.prototype.extractSeries = function(rawData, seriesIndex, options) {
var series = [];
var x, y, q0, q1, q2, q3, q4;
for (var j = 0; j < rawData.length; j++) {
x = rawData[j][0];
var data = rawData[j][seriesIndex];
y = data[0];
switch (data.length) {
case 3:
q0 = null;
q1 = data[1];
q2 = y;
q3 = data[2];
q4 = null;
break;
case 5:
q0 = data[1];
q1 = data[2];
q2 = y;
q3 = data[3];
q4 = data[4];
break;
case 6:
q0 = data[1];
q1 = data[2];
q2 = data[3]
q3 = data[4];
q4 = data[5];
break;
default:
q0 = null;
q1 = null;
q2 = null;
q3 = null;
q4 = null;
}

series.push([ x, y, [ q0, q1, q2, q3, q4 ] ]);
}
return series;
};

/** @inheritDoc */
BoxplotHandler.prototype.getExtremeYValues = function(series, dateWindow, options) {
var low = null, high = null;

for (var i = 0; i < series.length; i++) {
var y = series[i][1];
if (y === null || isNaN(y)) {
continue;
}

var q0 = series[i][2][0];
var q4 = series[i][2][4];

if ((low === null) || (q0 < low)) {
low = q0;
}
if ((high === null) || (q4 > high)) {
high = q4;
}
}

return [ low, high ];
};

/** @inheritDoc */
BoxplotHandler.prototype.onPointsCreated_ = function(series, points) {
for (var i = 0; i < series.length; ++i) {
var item = series[i];
var point = points[i];

point.box = [ DygraphDataHandler.parseFloat(item[2][1]),
DygraphDataHandler.parseFloat(item[2][2]),
DygraphDataHandler.parseFloat(item[2][3]) ];

point.whisker = [ DygraphDataHandler.parseFloat(item[2][0]),
DygraphDataHandler.parseFloat(item[2][4]) ]
}
};

/** @inheritDoc */
BoxplotHandler.prototype.onLineEvaluated = function(points, axis, logscale) {
for (var j = 0; j < points.length; j++) {
var p = points[j];
p.y_box = [ DygraphLayout.calcYNormal_(axis, p.box[0], logscale),
DygraphLayout.calcYNormal_(axis, p.box[1], logscale),
DygraphLayout.calcYNormal_(axis, p.box[2], logscale) ];

p.y_whisker = [ DygraphLayout.calcYNormal_(axis, p.whisker[0], logscale),
DygraphLayout.calcYNormal_(axis, p.whisker[1], logscale) ];
}
};

export default BoxplotHandler;
106 changes: 106 additions & 0 deletions src/dygraph-canvas.js
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,10 @@ DygraphCanvasRenderer._Plotters = {
fillPlotter: function(e) {
DygraphCanvasRenderer._fillPlotter(e);
},

boxplotPlotter: function(e) {
DygraphCanvasRenderer._boxplotPlotter(e);
},

errorPlotter: function(e) {
DygraphCanvasRenderer._errorPlotter(e);
Expand Down Expand Up @@ -540,6 +544,108 @@ DygraphCanvasRenderer._errorPlotter = function(e) {
};


/** Displays a boxplot for each datapoint.
*
* @author Vojtech Horky (vojtech.horky@gmail.com)
* @private
*/
DygraphCanvasRenderer._boxplotPlotter = function(e) {
if (!e.dygraph.getBooleanOption("boxplot")) {
return;
}

var drawWhisker = function(ctx, width, x, box_y, whisker_y) {
ctx.beginPath();
ctx.moveTo(x,box_y);
ctx.lineTo(x,whisker_y);
ctx.moveTo(x - width/2, whisker_y);
ctx.lineTo(x + width/2, whisker_y);
ctx.stroke();
};

var lighterColor = function(color) {
color.r = Math.floor(171 + color.r / 3);
color.g = Math.floor(171 + color.g / 3);
color.b = Math.floor(171 + color.b / 3);
return 'rgba(' + color.r + ',' + color.g + ',' + color.b + ',0.5)';
};

var recomputeY = function(e, normalizedY) {
return normalizedY * e.plotArea.h + e.plotArea.y;
};

var ctx = e.drawingContext;
var points = e.points;

// Ligher color for the box
ctx.fillStyle = lighterColor(Dygraph.toRGB_(e.color));

// Find pseudo-optimal bar width. Determine the minimal gap between
// two points.
var getMinimalGap = function(points) {
var minGap = Infinity;
var prevX = null;
for (var i = 0; i < points.length; i++) {
if (points[i] === null) {
continue;
}
if (prevX === null) {
prevX = points[i].canvasx;
continue;
}
var gap = points[i].canvasx - prevX;
if (gap < minGap) {
minGap = gap;
}
prevX = points[i].canvasx;
}
return minGap;
};

var drawBox = function(ctx, x, y, width, height, middleY) {
ctx.fillRect(x, y, width, height);
ctx.strokeRect(x, y, width, height);
ctx.beginPath();
ctx.moveTo(x, middleY);
ctx.lineTo(x + width, middleY);
ctx.stroke();
};

var drawWhisker = function(ctx, width, x, box_y, whisker_y) {
ctx.beginPath();
ctx.moveTo(x,box_y);
ctx.lineTo(x,whisker_y);
ctx.moveTo(x - width/2.0, whisker_y);
ctx.lineTo(x + width/2.0, whisker_y);
ctx.stroke();
};

var boxHalfWidth = getMinimalGap(points) / 3.0;

// Plot the actual points
for (var i = 0; i < points.length; i++) {
var p = points[i];

var boxBottom = recomputeY(e, p.y_box[0]);
var boxMiddle = recomputeY(e, p.y_box[1]);
var boxTop = recomputeY(e, p.y_box[2]);

var whiskerLow = recomputeY(e, p.y_whisker[0]);
var whiskerHigh = recomputeY(e, p.y_whisker[1]);

var x = p.canvasx;

drawBox(ctx, x - boxHalfWidth, boxTop,
2 * boxHalfWidth, boxBottom - boxTop, boxMiddle);

if (!isNaN(whiskerLow)) {
drawWhisker(ctx, boxHalfWidth, x, boxTop, whiskerHigh);
drawWhisker(ctx, boxHalfWidth, x, boxBottom, whiskerLow);
}
}
};


/**
* Proxy for CanvasRenderingContext2D which drops moveTo/lineTo calls which are
* superfluous. It accumulates all movements which haven't changed the x-value
Expand Down
2 changes: 2 additions & 0 deletions src/dygraph-default-attrs.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ var DEFAULT_ATTRS = {
fractions: false,
wilsonInterval: true, // only relevant if fractions is true
customBars: false,
boxplot: false,
fillGraph: false,
fillAlpha: 0.15,
connectSeparatedPoints: false,
Expand Down Expand Up @@ -93,6 +94,7 @@ var DEFAULT_ATTRS = {
plotter: [
DygraphCanvasRenderer._fillPlotter,
DygraphCanvasRenderer._errorPlotter,
DygraphCanvasRenderer._boxplotPlotter,
DygraphCanvasRenderer._linePlotter
],

Expand Down
7 changes: 7 additions & 0 deletions src/dygraph-options-reference.js
Original file line number Diff line number Diff line change
Expand Up @@ -581,6 +581,12 @@ OPTIONS_REFERENCE = // <JSON>
"type": "boolean",
"description": "When set, parse each CSV cell as \"low;middle;high\". Error bars will be drawn for each point between low and high, with the series itself going through middle."
},
"boxplot": {
"default": "false",
"labels": ["CSV parsing", "Boxplot"],
"type": "boolean",
"description": "When set, parse each CSV cell as \"mean;min;first-quartile;median;third-quartile;max\" or \"mean=median;min;first-quartile;third-quartile;max\" or \"mean=median;first-quartile;third-quartile\" (whiskers not drawn). Boxplot will be drawn for each point, with the series itself going through mean."
},
"colorValue": {
"default": "1.0",
"labels": ["Data Series Colors"],
Expand Down Expand Up @@ -858,6 +864,7 @@ var flds = ['type', 'default', 'description'];
var valid_cats = [
'Annotations',
'Axis display',
'Boxplot',
'Chart labels',
'CSV parsing',
'Callbacks',
Expand Down
Loading