From fd3bfdea5d55d68b1b929ea25c35a4ee8a04c97b Mon Sep 17 00:00:00 2001 From: Petr Shevtsov Date: Wed, 28 Jan 2015 17:10:19 +0300 Subject: [PATCH 01/16] Initial commit --- datahandler/compress.js | 245 ++++++++++++++++++++++++++++++++++ extras/candlestick-plotter.js | 79 +++++++++++ tests/candlestick.html | 52 ++++++++ 3 files changed, 376 insertions(+) create mode 100644 datahandler/compress.js create mode 100644 extras/candlestick-plotter.js create mode 100644 tests/candlestick.html diff --git a/datahandler/compress.js b/datahandler/compress.js new file mode 100644 index 000000000..71ae2ee9e --- /dev/null +++ b/datahandler/compress.js @@ -0,0 +1,245 @@ +/** + * @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"; + + Date.prototype.getWeek = function() { + var d = new Date(+this); + 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); + }; + Date.prototype.getWeekEndPoints = function(start) { + start = start || 0; + var today = new Date(this.setHours(0, 0, 0, 0)); + var day = today.getDay() - start; + var date = today.getDate() - day; + + var StartDate = new Date(today.setDate(date)); + var EndDate = new Date(today.setDate(date + 7)); + + return [StartDate, EndDate]; + }; + Date.prototype.getQuarter = function() { + var d = new Date(+this); + 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 = new Date(item[0]).getQuarter(); + 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 = new Date(item[0]).getWeekEndPoints(); + 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 = new Date(item[0]).getQuarter(); + break; + case "monthly": + currentPeriod = new Date(item[0]).getMonth(); + break; + case "weekly": + currentPeriod = new Date(item[0]).getWeek(); + 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; + }; + +})(); diff --git a/extras/candlestick-plotter.js b/extras/candlestick-plotter.js new file mode 100644 index 000000000..d99bf4906 --- /dev/null +++ b/extras/candlestick-plotter.js @@ -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); + } + }); +})(); diff --git a/tests/candlestick.html b/tests/candlestick.html new file mode 100644 index 000000000..0fd6ffd55 --- /dev/null +++ b/tests/candlestick.html @@ -0,0 +1,52 @@ + + + + + + Candlestick Chart Demo + + + + + + +
+ + + From a064dec78c54b81374bf7129782dc3c41edfb310 Mon Sep 17 00:00:00 2001 From: Petr Shevtsov Date: Wed, 28 Jan 2015 17:10:51 +0300 Subject: [PATCH 02/16] Adding compress data handler --- dygraph-dev.js | 3 ++- generate-combined.sh | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/dygraph-dev.js b/dygraph-dev.js index 26a96df96..5d519a347 100644 --- a/dygraph-dev.js +++ b/dygraph-dev.js @@ -41,7 +41,8 @@ "datahandler/bars.js", "datahandler/bars-error.js", "datahandler/bars-custom.js", - "datahandler/bars-fractions.js" + "datahandler/bars-fractions.js", + "datahandler/compress.js" ]; for (var i = 0; i < source_files.length; i++) { diff --git a/generate-combined.sh b/generate-combined.sh index 26d4a422e..01091e81f 100755 --- a/generate-combined.sh +++ b/generate-combined.sh @@ -32,7 +32,8 @@ GetSources () { datahandler/bars.js \ datahandler/bars-custom.js \ datahandler/bars-error.js \ - datahandler/bars-fractions.js + datahandler/bars-fractions.js \ + datahandler/compress.js do echo "$F" done From 9b78b9808ca327df3522b15734fbc84b5db04a5f Mon Sep 17 00:00:00 2001 From: Petr Shevtsov Date: Tue, 24 Mar 2015 01:56:11 +0300 Subject: [PATCH 03/16] Do not modify built-in objects --- datahandler/compress.js | 33 +++++++++++++++++++++------------ 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/datahandler/compress.js b/datahandler/compress.js index 71ae2ee9e..063070559 100644 --- a/datahandler/compress.js +++ b/datahandler/compress.js @@ -14,16 +14,22 @@ (function() { "use strict"; - Date.prototype.getWeek = function() { - var d = new Date(+this); + /** + * 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); }; - Date.prototype.getWeekEndPoints = function(start) { - start = start || 0; - var today = new Date(this.setHours(0, 0, 0, 0)); - var day = today.getDay() - start; + /** + * 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)); @@ -31,8 +37,11 @@ return [StartDate, EndDate]; }; - Date.prototype.getQuarter = function() { - var d = new Date(+this); + /** + * 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; }; @@ -118,7 +127,7 @@ endpoints.push(new Date(currentYear, 11, 31)); break; case "quarterly": - currentPeriod = new Date(item[0]).getQuarter(); + currentPeriod = getQuarter(new Date(item[0])); if (currentPeriod === 0) { endpoints.push(new Date(currentYear, 0, 1)); endpoints.push(new Date(currentYear, 2, 31)); @@ -139,7 +148,7 @@ endpoints.push(new Date(currentYear, currentPeriod + 1, 0)); break; case "weekly": - endpoints = new Date(item[0]).getWeekEndPoints(); + endpoints = getWeekEndPoints(new Date(item[0])); break; case "daily": endpoints = [new Date(item[0]), new Date(item[0])]; @@ -154,13 +163,13 @@ currentPeriod = new Date(item[0]).getFullYear(); break; case "quarterly": - currentPeriod = new Date(item[0]).getQuarter(); + currentPeriod = getQuarter(new Date(item[0])); break; case "monthly": currentPeriod = new Date(item[0]).getMonth(); break; case "weekly": - currentPeriod = new Date(item[0]).getWeek(); + currentPeriod = getWeek(new Date(item[0])); break; case "daily": currentPeriod = new Date(item[0]).getDay(); From cd387ccd66f5c14c48706cc6e47263594ec74047 Mon Sep 17 00:00:00 2001 From: Petr Shevtsov Date: Tue, 24 Mar 2015 01:59:06 +0300 Subject: [PATCH 04/16] Link compress data handler separately --- dygraph-dev.js | 3 +-- tests/candlestick.html | 1 + 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dygraph-dev.js b/dygraph-dev.js index 5d519a347..26a96df96 100644 --- a/dygraph-dev.js +++ b/dygraph-dev.js @@ -41,8 +41,7 @@ "datahandler/bars.js", "datahandler/bars-error.js", "datahandler/bars-custom.js", - "datahandler/bars-fractions.js", - "datahandler/compress.js" + "datahandler/bars-fractions.js" ]; for (var i = 0; i < source_files.length; i++) { diff --git a/tests/candlestick.html b/tests/candlestick.html index 0fd6ffd55..2c467a987 100644 --- a/tests/candlestick.html +++ b/tests/candlestick.html @@ -6,6 +6,7 @@ Candlestick Chart Demo + +

Candlestick chart

+

+ Candlesticks are traditional way of displaying price ranges as well as trend in + which a financial stock traded during a day (or week, month, etc.), from the + stock exchange open till close during the period, marking the + highest and lowest price reached during that period and + color-coding the trend. +

+

+ The data argument is a table containing a Date column and columns + with names Open, Close, High and + Low (in the exact order). High value is always the + highest of the four, Low value is always the lowest. If + Close is higher than Open for that period, the + candlestick will be green, otherwise it will be red. +

+ +

Candlestick chart with data compression

+

+ It is possible to compress data (annualy, quarterly, monthly, weekly or daily) + depending on the current chart zoom level to prevent chart bars overflow using + the CompressHandler data handler. +

+
+ + + @@ -39,10 +46,14 @@

Candlestick chart

Candlestick chart with data compression

It is possible to compress data (annualy, quarterly, monthly, weekly or daily) - depending on the current chart zoom level to prevent chart bars overflow using + depending on the current chart zoom level to prevent chart bars overflow using the CompressHandler data handler.

+ +

Line chart alongside with Candlestick chart

+
+ From c7e3192715a9aa7aec5b8178220418a85e962f64 Mon Sep 17 00:00:00 2001 From: Petr Shevtsov Date: Fri, 26 Feb 2016 12:02:47 +0600 Subject: [PATCH 15/16] Change parameters order to be the commonly used OHLC --- src/extras/candlestick-plotter.js | 12 ++++++------ tests/candlestick.html | 20 ++++++++++---------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/extras/candlestick-plotter.js b/src/extras/candlestick-plotter.js index 836306d41..6288aec4e 100644 --- a/src/extras/candlestick-plotter.js +++ b/src/extras/candlestick-plotter.js @@ -20,13 +20,13 @@ 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, + high : sets[1][p].yval, + low : sets[2][p].yval, + close : sets[3][p].yval, openY : sets[0][p].y, - closeY : sets[1][p].y, - highY : sets[2][p].y, - lowY : sets[3][p].y + highY : sets[1][p].y, + lowY : sets[2][p].y, + closeY : sets[3][p].y }; prices.push(price); } diff --git a/tests/candlestick.html b/tests/candlestick.html index 51bf23603..1af8ea017 100644 --- a/tests/candlestick.html +++ b/tests/candlestick.html @@ -56,7 +56,7 @@

Line chart alongside with Candlestick chart