Spaces:
Runtime error
Runtime error
| /*! | |
| * @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); | |
| }))); | |