Skip to content

Candlestick plotter #538

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 19 commits into
base: master
Choose a base branch
from
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
54 changes: 54 additions & 0 deletions auto_tests/tests/candlestick_plotter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/**
* @fileoverview Tests for the candlestick plotter.
*
* @author [email protected] (Petr Shevtsov)
*/

import Dygraph from '../../src/dygraph';
import '../../src/extras/candlestick-plotter'; // defines Dygraph.candlestickPlotter
import '../../src/extras/compress'; // defines Dygraph.DataHandlers.CompressHandler

describe("candlestick-plotter", function() {
var candlestickPlotter = Dygraph.candlestickPlotter;
var getPrices = candlestickPlotter._getPrices;

beforeEach(function() {
});

afterEach(function() {
});

it('testPrices', function() {
var set = [
[{yval: 200, y: 0.2}], // Open
[{yval: 400, y: 0.4}], // High
[{yval: 100, y: 0.1}], // Low
[{yval: 300, y: 0.3}] // Close
];

assert.deepEqual([{
open: 200,
openY: 0.2,
high: 400,
highY: 0.4,
low: 100,
lowY: 0.1,
close: 300,
closeY: 0.3
}], getPrices(set));
});

it('testCompressHandler', function() {
var series = [];
var x;
var y = 0;
for (var i = 1; i < 365; i++) {
x = i * 1000 * 60 * 60 * 24;
series.push([x, y]);
}
var comp = new Dygraph.DataHandlers.CompressHandler;
var pts = comp.seriesToPoints(series, "", 0);

assert.deepEqual(51, pts.length); // 50 bars + 1 to compensate
});
});
86 changes: 86 additions & 0 deletions src/extras/candlestick-plotter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/**
* The Candle chart plotter is adapted from code written by
* Zhenlei Cai ([email protected])
* https://github.com/danvk/dygraphs/pull/141/files
*/

(function() {
"use strict";

var Dygraph;
if (window.Dygraph) {
Dygraph = window.Dygraph;
} else if (typeof(module) !== 'undefined') {
Dygraph = require('../dygraph');
}

function getPrices(sets) {
var prices = [];
var price;
for (var p = 0 ; p < sets[0].length; p++) {
price = {
open : sets[0][p].yval,
high : sets[1][p].yval,
low : sets[2][p].yval,
close : sets[3][p].yval,
openY : sets[0][p].y,
highY : sets[1][p].y,
lowY : sets[2][p].y,
closeY : sets[3][p].y
};
prices.push(price);
}
return prices;
}

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

var sets = e.allSeriesPoints.slice(0, 4); // Slice first four sets for candlestick chart
var prices = getPrices(sets);
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);

var price;
for (var 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 = Math.floor(area.x + sets[0][p].x * area.w) + 0.5; // crisper rendering
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);
}
};
candlestickPlotter._getPrices = getPrices; // for testing
Dygraph.candlestickPlotter = candlestickPlotter;
})();
257 changes: 257 additions & 0 deletions src/extras/compress.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,257 @@
/**
* @license
* Copyright 2015 Petr Shevtsov ([email protected])
* 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.
*/

(function() {
"use strict";

var Dygraph;
if (window.Dygraph) {
Dygraph = window.Dygraph;
} else if (typeof(module) !== 'undefined') {
Dygraph = require('../dygraph');
}

/**
* 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 point = {
x : NaN,
y : NaN,
xval : item[0],
yval : item[1],
name : setName,
idx : i + boundaryIdStart
};
points.push(point);
}
this.onPointsCreated_(compressedSeries, points);
return points;
};

})();
Loading