/*! * @license * chartjs-chart-financial * http://chartjs.org/ * Version: 0.1.0 * * Copyright 2021 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'), require('chart.js/helpers')) : typeof define === 'function' && define.amd ? define(['chart.js', 'chart.js/helpers'], factory) : (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.Chart, global.Chart.helpers)); }(this, (function (chart_js, helpers) { 'use strict'; /** * Computes the "optimal" sample size to maintain bars equally sized while preventing overlap. * @private */ function computeMinSampleSize(scale, pixels) { let min = scale._length; let prev, curr, i, ilen; for (i = 1, ilen = pixels.length; i < ilen; ++i) { min = Math.min(min, Math.abs(pixels[i] - pixels[i - 1])); } for (i = 0, ilen = scale.ticks.length; i < ilen; ++i) { curr = scale.getPixelForTick(i); min = i > 0 ? Math.min(min, Math.abs(curr - prev)) : min; prev = curr; } return min; } /** * This class is based off controller.bar.js from the upstream Chart.js library */ class FinancialController extends chart_js.BarController { getLabelAndValue(index) { const me = this; const parsed = me.getParsed(index); const axis = me._cachedMeta.iScale.axis; const {o, h, l, c} = parsed; const value = `O: ${o} H: ${h} L: ${l} C: ${c}`; return { label: `${me._cachedMeta.iScale.getLabelForValue(parsed[axis])}`, value }; } getAllParsedValues() { const meta = this._cachedMeta; const axis = meta.iScale.axis; const parsed = meta._parsed; const values = []; for (let i = 0; i < parsed.length; ++i) { values.push(parsed[i][axis]); } return values; } /** * Implement this ourselves since it doesn't handle high and low values * https://github.com/chartjs/Chart.js/issues/7328 * @protected */ getMinMax(scale) { const meta = this._cachedMeta; const _parsed = meta._parsed; const axis = meta.iScale.axis; if (_parsed.length < 2) { return {min: 0, max: 1}; } if (scale === meta.iScale) { return {min: _parsed[0][axis], max: _parsed[_parsed.length - 1][axis]}; } let min = Number.POSITIVE_INFINITY; let max = Number.NEGATIVE_INFINITY; for (let i = 0; i < _parsed.length; i++) { const data = _parsed[i]; min = Math.min(min, data.l); max = Math.max(max, data.h); } return {min, max}; } _getRuler() { const me = this; const opts = me.options; const meta = me._cachedMeta; const iScale = meta.iScale; const axis = iScale.axis; const pixels = []; for (let i = 0; i < meta.data.length; ++i) { pixels.push(iScale.getPixelForValue(me.getParsed(i)[axis])); } const barThickness = opts.barThickness; const min = computeMinSampleSize(iScale, pixels); return { min, pixels, start: iScale._startPixel, end: iScale._endPixel, stackCount: me._getStackCount(), scale: iScale, ratio: barThickness ? 1 : opts.categoryPercentage * opts.barPercentage }; } /** * @protected */ calculateElementProperties(index, ruler, reset, options) { const me = this; const vscale = me._cachedMeta.vScale; const base = vscale.getBasePixel(); const ipixels = me._calculateBarIndexPixels(index, ruler, options); const data = me.chart.data.datasets[me.index].data[index]; const open = vscale.getPixelForValue(data.o); const high = vscale.getPixelForValue(data.h); const low = vscale.getPixelForValue(data.l); const close = vscale.getPixelForValue(data.c); return { base: reset ? base : low, x: ipixels.center, y: (low + high) / 2, width: ipixels.size, open, high, low, close }; } draw() { const me = this; const chart = me.chart; const rects = me._cachedMeta.data; helpers.clipArea(chart.ctx, chart.chartArea); for (let i = 0; i < rects.length; ++i) { rects[i].draw(me._ctx); } helpers.unclipArea(chart.ctx); } } FinancialController.overrides = { label: '', parsing: false, hover: { mode: 'label' }, datasets: { categoryPercentage: 0.8, barPercentage: 0.9, animation: { numbers: { type: 'number', properties: ['x', 'y', 'base', 'width', 'open', 'high', 'low', 'close'] } } }, scales: { x: { type: 'timeseries', offset: true, ticks: { major: { enabled: true, }, fontStyle: context => context.tick.major ? 'bold' : undefined, source: 'data', maxRotation: 0, autoSkip: true, autoSkipPadding: 75, sampleSize: 100 }, afterBuildTicks: scale => { const DateTime = window && window.luxon && window.luxon.DateTime; if (!DateTime) { return; } const majorUnit = scale._majorUnit; const ticks = scale.ticks; const firstTick = ticks[0]; if (!firstTick) { return; } let val = DateTime.fromMillis(firstTick.value); if ((majorUnit === 'minute' && val.second === 0) || (majorUnit === 'hour' && val.minute === 0) || (majorUnit === 'day' && val.hour === 9) || (majorUnit === 'month' && val.day <= 3 && val.weekday === 1) || (majorUnit === 'year' && val.month === 1)) { firstTick.major = true; } else { firstTick.major = false; } let lastMajor = val.get(majorUnit); for (let i = 1; i < ticks.length; i++) { const tick = ticks[i]; val = DateTime.fromMillis(tick.value); const currMajor = val.get(majorUnit); tick.major = currMajor !== lastMajor; lastMajor = currMajor; } scale.ticks = ticks; } }, y: { type: 'linear' } }, plugins: { tooltip: { intersect: false, mode: 'index', callbacks: { label(ctx) { const point = ctx.parsed; if (!helpers.isNullOrUndef(point.y)) { return chart_js.defaults.plugins.tooltip.callbacks.label(ctx); } const {o, h, l, c} = point; return `O: ${o} H: ${h} L: ${l} C: ${c}`; } } } } }; const globalOpts$2 = chart_js.Chart.defaults; globalOpts$2.elements.financial = { color: { up: 'rgba(255, 0, 0, 1)', down: 'rgba(0, 0, 255, 1)', unchanged: 'rgba(90, 90, 90, 1)', } }; /** * Helper function to get the bounds of the bar regardless of the orientation * @param {Rectangle} bar the bar * @param {boolean} [useFinalPosition] * @return {object} bounds of the bar * @private */ function getBarBounds(bar, useFinalPosition) { const {x, y, base, width, height} = bar.getProps(['x', 'low', 'high', 'width', 'height'], useFinalPosition); let left, right, top, bottom, half; if (bar.horizontal) { half = height / 2; left = Math.min(x, base); right = Math.max(x, base); top = y - half; bottom = y + half; } else { half = width / 2; left = x - half; right = x + half; top = Math.min(y, base); // use min because 0 pixel at top of screen bottom = Math.max(y, base); } return {left, top, right, bottom}; } function inRange(bar, x, y, useFinalPosition) { const skipX = x === null; const skipY = y === null; const bounds = !bar || (skipX && skipY) ? false : getBarBounds(bar, useFinalPosition); return bounds && (skipX || x >= bounds.left && x <= bounds.right) && (skipY || y >= bounds.top && y <= bounds.bottom); } class FinancialElement extends chart_js.Element { height() { return this.base - this.y; } inRange(mouseX, mouseY, useFinalPosition) { return inRange(this, mouseX, mouseY, useFinalPosition); } inXRange(mouseX, useFinalPosition) { return inRange(this, mouseX, null, useFinalPosition); } inYRange(mouseY, useFinalPosition) { return inRange(this, null, mouseY, useFinalPosition); } getRange(axis) { return axis === 'x' ? this.width / 2 : this.height / 2; } getCenterPoint(useFinalPosition) { const {x, low, high} = this.getProps(['x', 'low', 'high'], useFinalPosition); return { x, y: (high + low) / 2 }; } tooltipPosition(useFinalPosition) { const {x, open, close} = this.getProps(['x', 'open', 'close'], useFinalPosition); return { x, y: (open + close) / 2 }; } } const globalOpts$1 = chart_js.Chart.defaults; class CandlestickElement extends FinancialElement { draw(ctx) { const me = this; const {x, open, high, low, close} = me; let borderColors = me.borderColor; if (typeof borderColors === 'string') { borderColors = { up: borderColors, down: borderColors, unchanged: borderColors }; } let borderColor; if (close < open) { borderColor = helpers.valueOrDefault(borderColors ? borderColors.up : undefined, globalOpts$1.elements.candlestick.borderColor); ctx.fillStyle = helpers.valueOrDefault(me.color ? me.color.up : undefined, globalOpts$1.elements.candlestick.color.up); } else if (close > open) { borderColor = helpers.valueOrDefault(borderColors ? borderColors.down : undefined, globalOpts$1.elements.candlestick.borderColor); ctx.fillStyle = helpers.valueOrDefault(me.color ? me.color.down : undefined, globalOpts$1.elements.candlestick.color.down); } else { borderColor = helpers.valueOrDefault(borderColors ? borderColors.unchanged : undefined, globalOpts$1.elements.candlestick.borderColor); ctx.fillStyle = helpers.valueOrDefault(me.color ? me.color.unchanged : undefined, globalOpts$1.elements.candlestick.color.unchanged); } ctx.lineWidth = helpers.valueOrDefault(me.borderWidth, globalOpts$1.elements.candlestick.borderWidth); ctx.strokeStyle = helpers.valueOrDefault(borderColor, globalOpts$1.elements.candlestick.borderColor); ctx.beginPath(); ctx.moveTo(x, high); ctx.lineTo(x, Math.min(open, close)); ctx.moveTo(x, low); ctx.lineTo(x, Math.max(open, close)); ctx.stroke(); ctx.fillRect(x - me.width / 2, close, me.width, open - close); ctx.strokeRect(x - me.width / 2, close, me.width, open - close); ctx.closePath(); } } CandlestickElement.id = 'candlestick'; CandlestickElement.defaults = helpers.merge({}, [globalOpts$1.elements.financial, { borderColor: globalOpts$1.elements.financial.color.unchanged, borderWidth: 1, }]); class CandlestickController extends FinancialController { updateElements(elements, start, count, mode) { const me = this; const dataset = me.getDataset(); const ruler = me._ruler || me._getRuler(); const firstOpts = me.resolveDataElementOptions(start, mode); const sharedOptions = me.getSharedOptions(firstOpts); const includeOptions = me.includeOptions(mode, sharedOptions); me.updateSharedOptions(sharedOptions, mode, firstOpts); for (let i = start; i < count; i++) { const options = sharedOptions || me.resolveDataElementOptions(i, mode); const lineColor = (elements[i]['close'] - elements[i]['open'] < 0)? "rgb(255, 0, 0, 1)" : "rgb(0, 0, 255, 1)"; const baseProperties = me.calculateElementProperties(i, ruler, mode === 'reset', options); const properties = { ...baseProperties, datasetLabel: dataset.label || '', // label: '', // to get label value please use dataset.data[index].label // Appearance color: dataset.color, borderColor: lineColor, // borderColor: dataset.borderColor, borderWidth: dataset.borderWidth, }; if (includeOptions) { properties.options = options; } me.updateElement(elements[i], i, properties, mode); } } } CandlestickController.id = 'candlestick'; CandlestickController.defaults = helpers.merge({ dataElementType: CandlestickElement.id }, chart_js.Chart.defaults.financial); const globalOpts = chart_js.Chart.defaults; class OhlcElement extends FinancialElement { draw(ctx) { const me = this; const {x, open, high, low, close} = me; const armLengthRatio = helpers.valueOrDefault(me.armLengthRatio, globalOpts.elements.ohlc.armLengthRatio); let armLength = helpers.valueOrDefault(me.armLength, globalOpts.elements.ohlc.armLength); 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 = me.width * armLengthRatio * 0.5; } if (close < open) { ctx.strokeStyle = helpers.valueOrDefault(me.color ? me.color.up : undefined, globalOpts.elements.ohlc.color.up); } else if (close > open) { ctx.strokeStyle = helpers.valueOrDefault(me.color ? me.color.down : undefined, globalOpts.elements.ohlc.color.down); } else { ctx.strokeStyle = helpers.valueOrDefault(me.color ? me.color.unchanged : undefined, globalOpts.elements.ohlc.color.unchanged); } ctx.lineWidth = helpers.valueOrDefault(me.lineWidth, globalOpts.elements.ohlc.lineWidth); ctx.beginPath(); ctx.moveTo(x, high); ctx.lineTo(x, low); ctx.moveTo(x - armLength, open); ctx.lineTo(x, open); ctx.moveTo(x + armLength, close); ctx.lineTo(x, close); ctx.stroke(); } } OhlcElement.id = 'ohlc'; OhlcElement.defaults = helpers.merge({}, [globalOpts.elements.financial, { lineWidth: 2, armLength: null, armLengthRatio: 0.8, }]); class OhlcController extends FinancialController { updateElements(elements, start, count, mode) { const me = this; const dataset = me.getDataset(); const ruler = me._ruler || me._getRuler(); const firstOpts = me.resolveDataElementOptions(start, mode); const sharedOptions = me.getSharedOptions(firstOpts); const includeOptions = me.includeOptions(mode, sharedOptions); for (let i = 0; i < count; i++) { const options = sharedOptions || me.resolveDataElementOptions(i, mode); const baseProperties = me.calculateElementProperties(i, ruler, mode === 'reset', options); const properties = { ...baseProperties, datasetLabel: dataset.label || '', lineWidth: dataset.lineWidth, armLength: dataset.armLength, armLengthRatio: dataset.armLengthRatio, color: dataset.color, }; if (includeOptions) { properties.options = options; } me.updateElement(elements[i], i, properties, mode); } } } OhlcController.id = 'ohlc'; OhlcController.defaults = helpers.merge({ dataElementType: OhlcElement.id, datasets: { barPercentage: 1.0, categoryPercentage: 1.0 } }, chart_js.Chart.defaults.financial); chart_js.Chart.register(CandlestickController, OhlcController, CandlestickElement, OhlcElement); })));