/*! * @license * chartjs-chart-financial * http://chartjs.org/ * Version: 0.1.0 * * Copyright 2019 Chart.js Contributors * Released under the MIT license * https://github.com/chartjs/chartjs-chart-financial/blob/master/LICENSE.md */ (function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(require('chart.js')) : typeof define === 'function' && define.amd ? define(['chart.js'], factory) : (global = global || self, factory(global.Chart)); }(this, function (Chart) { 'use strict'; Chart = Chart && Chart.hasOwnProperty('default') ? Chart['default'] : Chart; var helpers = Chart.helpers; var defaultConfig = { position: 'left', ticks: { callback: Chart.Ticks.formatters.linear } }; var FinancialLinearScale = Chart.scaleService.getScaleConstructor('linear').extend({ determineDataLimits: function() { var me = this; var chart = me.chart; var data = chart.data; var datasets = data.datasets; var isHorizontal = me.isHorizontal(); function IDMatches(meta) { return isHorizontal ? meta.xAxisID === me.id : meta.yAxisID === me.id; } // First Calculate the range me.min = null; me.max = null; // Regular charts use x, y values // For the financial chart we have rawValue.h (hi) and rawValue.l (low) for each point helpers.each(datasets, function(dataset, datasetIndex) { var meta = chart.getDatasetMeta(datasetIndex); if (chart.isDatasetVisible(datasetIndex) && IDMatches(meta)) { helpers.each(dataset.data, function(rawValue) { var high = rawValue.h; var low = rawValue.l; if (me.min === null) { me.min = low; } else if (low < me.min) { me.min = low; } if (me.max === null) { me.max = high; } else if (high > me.max) { me.max = high; } }); } }); // Add whitespace around bars. Axis shouldn't go exactly from min to max var space = (me.max - me.min) * 0.05; me.min -= space; me.max += space; // Common base implementation to handle ticks.min, ticks.max, ticks.beginAtZero this.handleTickRangeOptions(); } }); Chart.scaleService.registerScaleType('financialLinear', FinancialLinearScale, defaultConfig); var helpers$1 = Chart.helpers; Chart.defaults.financial = { label: '', hover: { mode: 'label' }, scales: { xAxes: [{ type: 'time', distribution: 'series', categoryPercentage: 0.8, barPercentage: 0.9, ticks: { source: 'data' } }], yAxes: [{ type: 'financialLinear' }] }, tooltips: { intersect: false, mode: 'index', callbacks: { label: function(tooltipItem, data) { var p = data.datasets[tooltipItem.datasetIndex].data[tooltipItem.index].p; var o = data.datasets[tooltipItem.datasetIndex].data[tooltipItem.index].o; var h = data.datasets[tooltipItem.datasetIndex].data[tooltipItem.index].h; var l = data.datasets[tooltipItem.datasetIndex].data[tooltipItem.index].l; var c = data.datasets[tooltipItem.datasetIndex].data[tooltipItem.index].c; var dataset = data.datasets[tooltipItem.datasetIndex]; var precision = helpers$1.valueOrDefault(dataset.precision, 2); precision = Math.max(0, Math.min(100, precision)); o = o.toFixed(precision); h = h.toFixed(precision); l = l.toFixed(precision); c = c.toFixed(precision); return (p!=''?(p+'\r'):'') + ' O: ' + o + ' H: ' + h + ' L: ' + l + ' C: ' + c; } } } }; /** * This class is based off controller.bar.js from the upstream Chart.js library */ var FinancialController = Chart.controllers.bar.extend({ dataElementType: Chart.elements.Financial, /** * @private */ _updateElementGeometry: function(element, index, reset) { var me = this; var model = element._model; var vscale = me._getValueScale(); var base = vscale.getBasePixel(); var horizontal = vscale.isHorizontal(); var ruler = me._ruler || me.getRuler(); var vpixels = me.calculateBarValuePixels(me.index, index); var ipixels = me.calculateBarIndexPixels(me.index, index, ruler); var chart = me.chart; var datasets = chart.data.datasets; var indexData = datasets[me.index].data[index]; model.horizontal = horizontal; model.base = reset ? base : vpixels.base; model.x = horizontal ? reset ? base : vpixels.head : ipixels.center; model.y = horizontal ? ipixels.center : reset ? base : vpixels.head; model.height = horizontal ? ipixels.size : undefined; model.width = horizontal ? undefined : ipixels.size; model.candleOpen = vscale.getPixelForValue(Number(indexData.o)); model.candleHigh = vscale.getPixelForValue(Number(indexData.h)); model.candleLow = vscale.getPixelForValue(Number(indexData.l)); model.candleClose = vscale.getPixelForValue(Number(indexData.c)); }, draw: function() { var ctx = this.chart.chart.ctx; var elements = this.getMeta().data; var dataset = this.getDataset(); var ilen = elements.length; var i = 0; var d; Chart.canvasHelpers.clipArea(ctx, this.chart.chartArea); for (; i < ilen; ++i) { d = dataset.data[i].o; if (d !== null && d !== undefined && !isNaN(d)) { elements[i].draw(); } } Chart.canvasHelpers.unclipArea(ctx); }, }); var helpers$2 = Chart.helpers; var globalOpts = Chart.defaults.global; globalOpts.elements.financial = { color: { up: 'rgba(80, 160, 115, 1)', down: 'rgba(215, 85, 65, 1)', unchanged: 'rgba(90, 90, 90, 1)', } }; function isVertical(bar) { return bar._view.width !== undefined; } /** * Helper function to get the bounds of the candle * @private * @param bar {Chart.Element.financial} the bar * @return {Bounds} bounds of the bar */ function getBarBounds(candle) { var vm = candle._view; var x1, x2, y1, y2; var halfWidth = vm.width / 2; x1 = vm.x - halfWidth; x2 = vm.x + halfWidth; y1 = vm.candleHigh; y2 = vm.candleLow; return { left: x1, top: y1, right: x2, bottom: y2 }; } var FinancialElement = Chart.Element.extend({ height: function() { var vm = this._view; return vm.base - vm.y; }, inRange: function(mouseX, mouseY) { var inRange = false; if (this._view) { var bounds = getBarBounds(this); inRange = mouseX >= bounds.left && mouseX <= bounds.right && mouseY >= bounds.top && mouseY <= bounds.bottom; } return inRange; }, inLabelRange: function(mouseX, mouseY) { var me = this; if (!me._view) { return false; } var inRange = false; var bounds = getBarBounds(me); if (isVertical(me)) { inRange = mouseX >= bounds.left && mouseX <= bounds.right; } else { inRange = mouseY >= bounds.top && mouseY <= bounds.bottom; } return inRange; }, inXRange: function(mouseX) { var bounds = getBarBounds(this); return mouseX >= bounds.left && mouseX <= bounds.right; }, inYRange: function(mouseY) { var bounds = getBarBounds(this); return mouseY >= bounds.top && mouseY <= bounds.bottom; }, getCenterPoint: function() { var vm = this._view; return { x: vm.x, y: (vm.candleHigh + vm.candleLow) / 2 }; }, getArea: function() { var vm = this._view; return vm.width * Math.abs(vm.y - vm.base); }, tooltipPosition: function() { var vm = this._view; return { x: vm.x, y: (vm.candleOpen + vm.candleClose) / 2 }; }, hasValue: function() { var model = this._model; return helpers$2.isNumber(model.x) && helpers$2.isNumber(model.candleOpen) && helpers$2.isNumber(model.candleHigh) && helpers$2.isNumber(model.candleLow) && helpers$2.isNumber(model.candleClose); } }); var helpers$3 = Chart.helpers; var globalOpts$1 = Chart.defaults.global; globalOpts$1.elements.candlestick = helpers$3.merge({}, [globalOpts$1.elements.financial, { borderColor: globalOpts$1.elements.financial.color.unchanged, borderWidth: 1, }]); var CandlestickElement = FinancialElement.extend({ draw: function() { var ctx = this._chart.ctx; var vm = this._view; var x = vm.x; var o = vm.candleOpen; var h = vm.candleHigh; var l = vm.candleLow; var c = vm.candleClose; var borderColors = vm.borderColor; if (typeof borderColors === 'string') { borderColors = { up: borderColors, down: borderColors, unchanged: borderColors }; } var borderColor; if (c < o) { borderColor = helpers$3.getValueOrDefault(borderColors ? borderColors.up : undefined, globalOpts$1.elements.candlestick.borderColor); ctx.fillStyle = helpers$3.getValueOrDefault(vm.color ? vm.color.up : undefined, globalOpts$1.elements.candlestick.color.up); } else if (c > o) { borderColor = helpers$3.getValueOrDefault(borderColors ? borderColors.down : undefined, globalOpts$1.elements.candlestick.borderColor); ctx.fillStyle = helpers$3.getValueOrDefault(vm.color ? vm.color.down : undefined, globalOpts$1.elements.candlestick.color.down); } else { borderColor = helpers$3.getValueOrDefault(borderColors ? borderColors.unchanged : undefined, globalOpts$1.elements.candlestick.borderColor); ctx.fillStyle = helpers$3.getValueOrDefault(vm.color ? vm.color.unchanged : undefined, globalOpts$1.elements.candlestick.color.unchanged); } ctx.lineWidth = helpers$3.getValueOrDefault(vm.borderWidth, globalOpts$1.elements.candlestick.borderWidth); ctx.strokeStyle = helpers$3.getValueOrDefault(borderColor, globalOpts$1.elements.candlestick.borderColor); ctx.beginPath(); ctx.moveTo(x, h); ctx.lineTo(x, Math.min(o, c)); ctx.moveTo(x, l); ctx.lineTo(x, Math.max(o, c)); ctx.stroke(); ctx.fillRect(x - vm.width / 2, c, vm.width, o - c); ctx.strokeRect(x - vm.width / 2, c, vm.width, o - c); ctx.closePath(); }, }); Chart.defaults.candlestick = Chart.helpers.merge({}, Chart.defaults.financial); var CandlestickController = Chart.controllers.candlestick = FinancialController.extend({ dataElementType: CandlestickElement, updateElement: function(element, index, reset) { var me = this; var meta = me.getMeta(); var dataset = me.getDataset(); element._xScale = me.getScaleForId(meta.xAxisID); element._yScale = me.getScaleForId(meta.yAxisID); element._datasetIndex = me.index; element._index = index; element._model = { datasetLabel: dataset.label || '', // label: '', // to get label value please use dataset.data[index].label // Appearance color: dataset.color, borderColor: dataset.borderColor, borderWidth: dataset.borderWidth, }; me._updateElementGeometry(element, index, reset); element.pivot(); }, }); var helpers$4 = Chart.helpers; var globalOpts$2 = Chart.defaults.global; globalOpts$2.elements.ohlc = helpers$4.merge({}, [globalOpts$2.elements.financial, { lineWidth: 2, armLength: null, armLengthRatio: 0.8, }]); var OhlcElement = FinancialElement.extend({ draw: function() { var ctx = this._chart.ctx; var vm = this._view; var x = vm.x; var o = vm.candleOpen; var h = vm.candleHigh; var l = vm.candleLow; var c = vm.candleClose; var armLength = helpers$4.getValueOrDefault(vm.armLength, globalOpts$2.elements.ohlc.armLength); var armLengthRatio = helpers$4.getValueOrDefault(vm.armLengthRatio, globalOpts$2.elements.ohlc.armLengthRatio); if (armLength === null) { // The width of an ohlc is affected by barPercentage and categoryPercentage // This behavior is caused by extending controller.financial, which extends controller.bar // barPercentage and categoryPercentage are now set to 1.0 (see controller.ohlc) // and armLengthRatio is multipled by 0.5, // so that when armLengthRatio=1.0, the arms from neighbour ohcl touch, // and when armLengthRatio=0.0, ohcl are just vertical lines. armLength = vm.width * armLengthRatio * 0.5; } if (c < o) { ctx.strokeStyle = helpers$4.getValueOrDefault(vm.color ? vm.color.up : undefined, globalOpts$2.elements.ohlc.color.up); } else if (c > o) { ctx.strokeStyle = helpers$4.getValueOrDefault(vm.color ? vm.color.down : undefined, globalOpts$2.elements.ohlc.color.down); } else { ctx.strokeStyle = helpers$4.getValueOrDefault(vm.color ? vm.color.unchanged : undefined, globalOpts$2.elements.ohlc.color.unchanged); } ctx.lineWidth = helpers$4.getValueOrDefault(vm.lineWidth, globalOpts$2.elements.ohlc.lineWidth); ctx.beginPath(); ctx.moveTo(x, h); ctx.lineTo(x, l); ctx.moveTo(x - armLength, o); ctx.lineTo(x, o); ctx.moveTo(x + armLength, c); ctx.lineTo(x, c); ctx.stroke(); }, }); Chart.defaults.ohlc = Chart.helpers.merge({}, Chart.defaults.financial); Chart.defaults.ohlc.scales.xAxes[0].barPercentage = 1.0; Chart.defaults.ohlc.scales.xAxes[0].categoryPercentage = 1.0; var OhlcController = Chart.controllers.ohlc = FinancialController.extend({ dataElementType: OhlcElement, updateElement: function(element, index, reset) { var me = this; var meta = me.getMeta(); var dataset = me.getDataset(); element._xScale = me.getScaleForId(meta.xAxisID); element._yScale = me.getScaleForId(meta.yAxisID); element._datasetIndex = me.index; element._index = index; element._model = { datasetLabel: dataset.label || '', lineWidth: dataset.lineWidth, armLength: dataset.armLength, armLengthRatio: dataset.armLengthRatio, color: dataset.color, }; me._updateElementGeometry(element, index, reset); element.pivot(); }, }); }));