Skip to content
Open
Show file tree
Hide file tree
Changes from 8 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
254 changes: 254 additions & 0 deletions src/datahandler/compress.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,254 @@
/**
* @license
* Copyright 2015 Petr Shevtsov (petr.shevtsov@gmail.com)
* MIT-licensed (http://opensource.org/licenses/MIT)
*
* Compress data handler "compresses" chart data annually, quarterly, monthly,
* weekly or daily.
*
* See "tests/candlestick.html" for demo.
*/

/*global Dygraph:false */

(function() {
"use strict";

/**
* Get week number for date
*/
var getWeek = function(d) {
var d = new Date(+d);
d.setHours(0,0,0);
d.setDate(d.getDate()+4-(d.getDay()||7));
return Math.ceil((((d-new Date(d.getFullYear(),0,1))/8.64e7)+1)/7);
};
/**
* Get week endpoints (i.e. start of the week and end of the week dates) for date
*/
var getWeekEndPoints = function(d) {
var d = new Date(+d);
var today = new Date(d.setHours(0, 0, 0, 0));
var day = today.getDay();
var date = today.getDate() - day;

var StartDate = new Date(today.setDate(date));
var EndDate = new Date(today.setDate(date + 7));

return [StartDate, EndDate];
};
/**
* Get quarter for date
*/
var getQuarter = function(d) {
var d = new Date(+d);
var m = Math.floor(d.getMonth()/3) + 2;
return m > 4 ? m - 5 : m;
};
Dygraph.DataHandlers.CompressHandler = function() {};
var CompressHandler = Dygraph.DataHandlers.CompressHandler;
CompressHandler.prototype = new Dygraph.DataHandlers.DefaultHandler();
CompressHandler.prototype.seriesToPoints = function(series, setName, boundaryIdStart) {
var compress = {
titles: [
"annually",
"quarterly",
"monthly",
"weekly",
"daily"
],
days: [
365,
90,
30,
7,
1
],
bars: [],
barsRange: [20, 50]
};
var firstItem = series[0];
var lastItem = series[series.length - 1];
var dateDiff = lastItem[0] - firstItem[0];
var dayMs = 1000 * 60 * 60 * 24;
var compressedSeries = [];
var ratio = 1;
var points = [];
var bounds = [];
var idx;
var compressTitle;

for (var i = 0; i < compress.days.length; i++) {
ratio = compress.days[i] * dayMs;
var bars = Math.round(dateDiff / ratio);
compress.bars.push(bars);
}

idx = compress.bars.reduce(function(previous, current, index) {
if (current < compress.barsRange[1]) {
return index;
}
return previous;
}, 0);
bounds.push(idx);

idx = compress.bars.reduceRight(function(previous, current, index) {
if (current > compress.barsRange[0]) {
return index;
}
return previous;
}, compress.bars.length - 1);
bounds.push(idx);

if (bounds[0] === bounds[1]) {
compressTitle = compress.titles[bounds[0]];
} else {
var lower_idx = bounds[0];
var lower = Math.abs(compress.bars[idx] - compress.barsRange[0]);
var higher_idx = bounds[1];
var higher = Math.abs(compress.bars[idx] - compress.barsRange[1]);
var min = Math.min(lower, higher);
idx = (min === lower) ? lower_idx : higher_idx;
compressTitle = compress.titles[idx];
}

var doCompress = function(title) {
var period;
var currentPeriod;
var buffer = [];
var compressed = [];
var getPeriodEndPoints = function(period, date) {
var endpoints = [];
var currentYear = new Date(date).getFullYear();
var currentPeriod;
switch (period) {
case "annually":
endpoints.push(new Date(currentYear, 0, 1));
endpoints.push(new Date(currentYear, 11, 31));
break;
case "quarterly":
currentPeriod = getQuarter(new Date(item[0]));
if (currentPeriod === 0) {
endpoints.push(new Date(currentYear, 0, 1));
endpoints.push(new Date(currentYear, 2, 31));
} else if (currentPeriod === 1) {
endpoints.push(new Date(currentYear, 3, 1));
endpoints.push(new Date(currentYear, 5, 30));
} else if (currentPeriod === 2) {
endpoints.push(new Date(currentYear, 6, 1));
endpoints.push(new Date(currentYear, 8, 30));
} else {
endpoints.push(new Date(currentYear, 9, 1));
endpoints.push(new Date(currentYear, 11, 31));
}
break;
case "monthly":
currentPeriod = new Date(item[0]).getMonth();
endpoints.push(new Date(currentYear, currentPeriod, 1));
endpoints.push(new Date(currentYear, currentPeriod + 1, 0));
break;
case "weekly":
endpoints = getWeekEndPoints(new Date(item[0]));
break;
case "daily":
endpoints = [new Date(item[0]), new Date(item[0])];
break;
}
return endpoints;
};
for (var i = 0; i < series.length; i++) {
var item = series[i];
switch (title) {
case "annually":
currentPeriod = new Date(item[0]).getFullYear();
break;
case "quarterly":
currentPeriod = getQuarter(new Date(item[0]));
break;
case "monthly":
currentPeriod = new Date(item[0]).getMonth();
break;
case "weekly":
currentPeriod = getWeek(new Date(item[0]));
break;
case "daily":
currentPeriod = new Date(item[0]).getDay();
break;
}
if (period === undefined) {
period = currentPeriod;
}
if (period === currentPeriod) {
buffer.push(item);
} else {
compressed.push(buffer);
buffer = [];
buffer.push(item);
period = currentPeriod;
}
}
return compressed.reduce(function(prev, curr, index) {
var date = curr[curr.length - 1][0];
var value;

// Check if we have more or less full period for the first bar
if (index === 0) {
var endpoints = getPeriodEndPoints(title, curr[0][0]);
var range = [];
range.push(curr[0][0]); // first date
range.push(date); // last date

if (endpoints[0] !== range[0] || endpoints[1] !== range[1]) {
return prev;
}
}

switch (setName.toLowerCase()) {
case "open":
value = curr[0][1]; // Open of the first day
break;
case "high":
// Highest High of all the daily Highs
value = Math.max.apply(null, curr.reduce(function(p, c) {
p.push(c[1]);
return p;
}, []));
break;
case "low":
// Lowest Low of all the daily Lows
value = Math.min.apply(null, curr.reduce(function(p, c) {
p.push(c[1]);
return p;
}, []));
break;
case "close":
value = curr[curr.length - 1][1]; // Close of the last day
break;
}
prev.push([date, value]);

return prev;
}, []);
};

compressedSeries = doCompress(compressTitle);

for (i = 0; i < compressedSeries.length; ++i) {
var item = compressedSeries[i];
var yraw = item[1];
var yval = yraw === null ? null : Dygraph.DataHandler.parseFloat(yraw);
var point = {
x : NaN,
y : NaN,
xval : Dygraph.DataHandler.parseFloat(item[0]),
yval : yval,
name : setName,
idx : i + boundaryIdStart
};
points.push(point);
}
this.onPointsCreated_(compressedSeries, points);
return points;
};

})();
79 changes: 79 additions & 0 deletions src/extras/candlestick-plotter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/**
* The Candle chart plotter is adapted from code written by
* Zhenlei Cai (jpenguin@gmail.com)
* https://github.com/danvk/dygraphs/pull/141/files
*/

/*global Dygraph:false */

(function() {
"use strict";

var candlePlotter = function(e) {
if (e.seriesIndex > 3) {
Dygraph.Plotters.linePlotter(e);
}
// This is the officially endorsed way to plot all the series at once.
if (e.seriesIndex !== 0) return;

var prices = [];
var price;
var sets = e.allSeriesPoints.slice(0, 4); // Slice first four sets for candlestick chart
for (var p = 0 ; p < sets[0].length; p++) {
price = {
open : sets[0][p].yval,
close : sets[1][p].yval,
high : sets[2][p].yval,
low : sets[3][p].yval,
openY : sets[0][p].y,
closeY : sets[1][p].y,
highY : sets[2][p].y,
lowY : sets[3][p].y
};
prices.push(price);
}

var area = e.plotArea;
var ctx = e.drawingContext;
ctx.strokeStyle = '#202020';
ctx.lineWidth = 0.6;

var minBarWidth = 2;
var numBars = prices.length + 1; // To compensate the probably removed first "incomplete" bar
var barWidth = Math.round((area.w / numBars) / 2);
if (barWidth % 2 !== 0) {
barWidth++;
}
barWidth = Math.max(barWidth, minBarWidth);

for (p = 0 ; p < prices.length; p++) {
ctx.beginPath();

price = prices[p];
var topY = area.h * price.highY + area.y;
var bottomY = area.h * price.lowY + area.y;
var centerX = area.x + sets[0][p].x * area.w;
ctx.moveTo(centerX, topY);
ctx.lineTo(centerX, bottomY);
ctx.closePath();
ctx.stroke();
var bodyY;
if (price.open > price.close) {
ctx.fillStyle ='rgba(244,44,44,1.0)';
bodyY = area.h * price.openY + area.y;
}
else {
ctx.fillStyle ='rgba(44,244,44,1.0)';
bodyY = area.h * price.closeY + area.y;
}
var bodyHeight = area.h * Math.abs(price.openY - price.closeY);
ctx.fillRect(centerX - barWidth / 2, bodyY, barWidth, bodyHeight);
}
};

Dygraph.update(Dygraph.Plotters, {
candlePlotter: function(e) {
candlePlotter(e);
}
});
})();
53 changes: 53 additions & 0 deletions tests/candlestick.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@

<!DOCTYPE html>
<html>
<head>
<meta http-equiv="X-UA-Compatible" content="IE=EmulateIE7; IE=EmulateIE9">
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove this—it's a relic of when dygraphs supported canvas emulation.

<title>Candlestick Chart Demo</title>
<script type="text/javascript" src="../dygraph-dev.js"></script>
<script type="text/javascript" src="../src/extras/candlestick-plotter.js"></script>
<script type="text/javascript" src="../src/datahandler/compress.js"></script>

<style type="text/css">
body {
max-width: 750px;
}
div.chart {
width: 640px;
height: 320px;
}
</style>
</head>
<body>
<div id="candlechart" class=chart></div>
<script type="text/javascript">
var candleData =
"Date,Open,Close,High,Low\n";
for (var i = 0; i <= 365 * 10; i++) {
var d = new Date(i
* 1000 // ms
* 60 // sec
* 60 // min
* 24 // hr
);
var dateStr = d.toISOString().match(/^\d{4}-\d{2}-\d{2}/)[0];
var min = 100;
var max = 400;
var open = Math.random() * (max - min) + min;
var close = Math.random() * (max - min) + min;
var high = Math.max(open, close) * 1.25;
var low = Math.min(open, close) * 0.75;

candleData += [dateStr, open, close, high, low].join(",") + "\n";
}
g = new Dygraph(
document.getElementById("candlechart"),
candleData,
{
plotter: Dygraph.Plotters.candlePlotter,
dataHandler: Dygraph.DataHandlers.CompressHandler
}
);
</script>
</body>
</html>