424 lines
12 KiB
JavaScript
424 lines
12 KiB
JavaScript
/*!
|
|
* @license
|
|
* chartjs-chart-financial
|
|
* http://chartjs.org/
|
|
* Version: 0.2.0
|
|
*
|
|
* Copyright 2024 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';
|
|
|
|
/**
|
|
* This class is based off controller.bar.js from the upstream Chart.js library
|
|
*/
|
|
class FinancialController extends chart_js.BarController {
|
|
|
|
static overrides = {
|
|
label: '',
|
|
|
|
parsing: false,
|
|
|
|
hover: {
|
|
mode: 'label'
|
|
},
|
|
animations: {
|
|
numbers: {
|
|
type: 'number',
|
|
properties: ['x', 'y', 'base', 'width', 'open', 'high', 'low', 'close']
|
|
}
|
|
},
|
|
|
|
scales: {
|
|
x: {
|
|
type: 'timeseries',
|
|
offset: true,
|
|
ticks: {
|
|
major: {
|
|
enabled: true,
|
|
},
|
|
source: 'data',
|
|
maxRotation: 0,
|
|
autoSkip: true,
|
|
autoSkipPadding: 75,
|
|
sampleSize: 100
|
|
},
|
|
},
|
|
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}`;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
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
|
|
};
|
|
}
|
|
|
|
getUserBounds(scale) {
|
|
const {min, max, minDefined, maxDefined} = scale.getUserBounds();
|
|
return {
|
|
min: minDefined ? min : Number.NEGATIVE_INFINITY,
|
|
max: maxDefined ? max : Number.POSITIVE_INFINITY
|
|
};
|
|
}
|
|
|
|
/**
|
|
* 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;
|
|
const otherScale = this._getOtherScale(scale);
|
|
const {min: otherMin, max: otherMax} = this.getUserBounds(otherScale);
|
|
|
|
if (_parsed.length < 2) {
|
|
return {min: 0, max: 1};
|
|
}
|
|
|
|
if (scale === meta.iScale) {
|
|
return {min: _parsed[0][axis], max: _parsed[_parsed.length - 1][axis]};
|
|
}
|
|
|
|
const newParsedData = _parsed.filter(({x}) => x >= otherMin && x < otherMax);
|
|
|
|
let min = Number.POSITIVE_INFINITY;
|
|
let max = Number.NEGATIVE_INFINITY;
|
|
for (let i = 0; i < newParsedData.length; i++) {
|
|
const data = newParsedData[i];
|
|
min = Math.min(min, data.l);
|
|
max = Math.max(max, data.h);
|
|
}
|
|
return {min, max};
|
|
}
|
|
|
|
/**
|
|
* @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);
|
|
}
|
|
|
|
}
|
|
|
|
/**
|
|
* 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.BarElement {
|
|
|
|
static defaults = {
|
|
backgroundColors: {
|
|
up: 'rgba(75, 192, 192, 0.5)',
|
|
down: 'rgba(255, 99, 132, 0.5)',
|
|
unchanged: 'rgba(201, 203, 207, 0.5)',
|
|
},
|
|
borderColors: {
|
|
up: 'rgb(75, 192, 192)',
|
|
down: 'rgb(255, 99, 132)',
|
|
unchanged: 'rgb(201, 203, 207)',
|
|
}
|
|
};
|
|
|
|
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
|
|
};
|
|
}
|
|
}
|
|
|
|
class CandlestickElement extends FinancialElement {
|
|
static id = 'candlestick';
|
|
|
|
static defaults = {
|
|
...FinancialElement.defaults,
|
|
borderWidth: 1,
|
|
};
|
|
|
|
draw(ctx) {
|
|
const me = this;
|
|
|
|
const {x, open, high, low, close} = me;
|
|
|
|
let borderColors = me.options.borderColors;
|
|
if (typeof borderColors === 'string') {
|
|
borderColors = {
|
|
up: borderColors,
|
|
down: borderColors,
|
|
unchanged: borderColors
|
|
};
|
|
}
|
|
|
|
let borderColor;
|
|
if (close < open) {
|
|
borderColor = helpers.valueOrDefault(borderColors ? borderColors.up : undefined, chart_js.defaults.elements.candlestick.borderColors.up);
|
|
ctx.fillStyle = helpers.valueOrDefault(me.options.backgroundColors ? me.options.backgroundColors.up : undefined, chart_js.defaults.elements.candlestick.backgroundColors.up);
|
|
} else if (close > open) {
|
|
borderColor = helpers.valueOrDefault(borderColors ? borderColors.down : undefined, chart_js.defaults.elements.candlestick.borderColors.down);
|
|
ctx.fillStyle = helpers.valueOrDefault(me.options.backgroundColors ? me.options.backgroundColors.down : undefined, chart_js.defaults.elements.candlestick.backgroundColors.down);
|
|
} else {
|
|
borderColor = helpers.valueOrDefault(borderColors ? borderColors.unchanged : undefined, chart_js.defaults.elements.candlestick.borderColors.unchanged);
|
|
ctx.fillStyle = helpers.valueOrDefault(me.backgroundColors ? me.backgroundColors.unchanged : undefined, chart_js.defaults.elements.candlestick.backgroundColors.unchanged);
|
|
}
|
|
|
|
ctx.lineWidth = helpers.valueOrDefault(me.options.borderWidth, chart_js.defaults.elements.candlestick.borderWidth);
|
|
ctx.strokeStyle = 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();
|
|
}
|
|
}
|
|
|
|
class CandlestickController extends FinancialController {
|
|
|
|
static id = 'candlestick';
|
|
|
|
static defaults = {
|
|
...FinancialController.defaults,
|
|
dataElementType: CandlestickElement.id
|
|
};
|
|
|
|
static defaultRoutes = chart_js.BarController.defaultRoutes;
|
|
|
|
updateElements(elements, start, count, mode) {
|
|
const reset = mode === 'reset';
|
|
const ruler = this._getRuler();
|
|
const {sharedOptions, includeOptions} = this._getSharedOptions(start, mode);
|
|
|
|
for (let i = start; i < start + count; i++) {
|
|
const options = sharedOptions || this.resolveDataElementOptions(i, mode);
|
|
|
|
const baseProperties = this.calculateElementProperties(i, ruler, reset, options);
|
|
|
|
if (includeOptions) {
|
|
baseProperties.options = options;
|
|
}
|
|
this.updateElement(elements[i], i, baseProperties, mode);
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
const defaults = chart_js.Chart.defaults;
|
|
|
|
class OhlcElement extends FinancialElement {
|
|
static id = 'ohlc';
|
|
|
|
static defaults = {
|
|
...FinancialElement.defaults,
|
|
lineWidth: 2,
|
|
armLength: null,
|
|
armLengthRatio: 0.8
|
|
};
|
|
|
|
draw(ctx) {
|
|
const me = this;
|
|
|
|
const {x, open, high, low, close} = me;
|
|
|
|
const armLengthRatio = helpers.valueOrDefault(me.armLengthRatio, defaults.elements.ohlc.armLengthRatio);
|
|
let armLength = helpers.valueOrDefault(me.armLength, defaults.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.options.borderColors ? me.options.borderColors.up : undefined, defaults.elements.ohlc.borderColors.up);
|
|
} else if (close > open) {
|
|
ctx.strokeStyle = helpers.valueOrDefault(me.options.borderColors ? me.options.borderColors.down : undefined, defaults.elements.ohlc.borderColors.down);
|
|
} else {
|
|
ctx.strokeStyle = helpers.valueOrDefault(me.options.borderColors ? me.options.borderColors.unchanged : undefined, defaults.elements.ohlc.borderColors.unchanged);
|
|
}
|
|
ctx.lineWidth = helpers.valueOrDefault(me.lineWidth, defaults.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();
|
|
}
|
|
}
|
|
|
|
class OhlcController extends FinancialController {
|
|
static id = 'ohlc';
|
|
|
|
static defaults = {
|
|
...FinancialController.defaults,
|
|
dataElementType: OhlcElement.id,
|
|
datasets: {
|
|
barPercentage: 1.0,
|
|
categoryPercentage: 1.0
|
|
}
|
|
};
|
|
|
|
updateElements(elements, start, count, mode) {
|
|
const reset = mode === 'reset';
|
|
const ruler = this._getRuler();
|
|
const {sharedOptions, includeOptions} = this._getSharedOptions(start, mode);
|
|
|
|
for (let i = start; i < start + count; i++) {
|
|
const options = sharedOptions || this.resolveDataElementOptions(i, mode);
|
|
|
|
const baseProperties = this.calculateElementProperties(i, ruler, reset, options);
|
|
|
|
if (includeOptions) {
|
|
baseProperties.options = options;
|
|
}
|
|
this.updateElement(elements[i], i, baseProperties, mode);
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
chart_js.Chart.register(CandlestickController, OhlcController, CandlestickElement, OhlcElement);
|
|
|
|
}));
|