highcharts
Version:
JavaScript charting framework
649 lines (648 loc) • 24.1 kB
JavaScript
/* *
*
* (c) 2010-2025 Torstein Honsi
*
* License: www.highcharts.com/license
*
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
*
* */
;
import Chart from '../Chart/Chart.js';
import F from '../Templating.js';
const { format } = F;
import D from '../Defaults.js';
const { getOptions } = D;
import NavigatorDefaults from '../../Stock/Navigator/NavigatorDefaults.js';
import RangeSelectorDefaults from '../../Stock/RangeSelector/RangeSelectorDefaults.js';
import ScrollbarDefaults from '../../Stock/Scrollbar/ScrollbarDefaults.js';
import StockUtilities from '../../Stock/Utilities/StockUtilities.js';
const { setFixedRange } = StockUtilities;
import U from '../Utilities.js';
const { addEvent, clamp, crisp, defined, extend, find, isNumber, isString, merge, pick, splat } = U;
/* *
*
* Functions
*
* */
/**
* Get stock-specific default axis options.
*
* @private
* @function getDefaultAxisOptions
*/
function getDefaultAxisOptions(coll, options, defaultOptions) {
if (coll === 'xAxis') {
return {
minPadding: 0,
maxPadding: 0,
overscroll: 0,
ordinal: true
};
}
if (coll === 'yAxis') {
return {
labels: {
y: -2
},
opposite: defaultOptions.opposite ?? options.opposite ?? true,
showLastLabel: !!(
// #6104, show last label by default for category axes
options.categories ||
options.type === 'category'),
title: {
text: void 0
}
};
}
return {};
}
/**
* Get stock-specific forced axis options.
*
* @private
* @function getForcedAxisOptions
*/
function getForcedAxisOptions(type, chartOptions) {
if (type === 'xAxis') {
// Always disable startOnTick:true on the main axis when the navigator
// is enabled (#1090)
const navigatorEnabled = pick(chartOptions.navigator?.enabled, NavigatorDefaults.enabled, true);
const axisOptions = {
type: 'datetime',
categories: void 0
};
if (navigatorEnabled) {
axisOptions.startOnTick = false;
axisOptions.endOnTick = false;
}
return axisOptions;
}
return {};
}
/* *
*
* Class
*
* */
/**
* Stock-optimized chart. Use {@link Highcharts.Chart|Chart} for common charts.
*
* @requires modules/stock
*
* @class
* @name Highcharts.StockChart
* @extends Highcharts.Chart
*/
class StockChart extends Chart {
/* *
*
* Functions
*
* */
/**
* Initializes the chart. The constructor's arguments are passed on
* directly.
*
* @function Highcharts.StockChart#init
*
* @param {Highcharts.Options} userOptions
* Custom options.
*
* @param {Function} [callback]
* Function to run when the chart has loaded and all external
* images are loaded.
*
*
* @emits Highcharts.StockChart#event:init
* @emits Highcharts.StockChart#event:afterInit
*/
init(userOptions, callback) {
const defaultOptions = getOptions(), xAxisOptions = userOptions.xAxis, yAxisOptions = userOptions.yAxis,
// Always disable startOnTick:true on the main axis when the
// navigator is enabled (#1090)
navigatorEnabled = pick(userOptions.navigator?.enabled, NavigatorDefaults.enabled, true);
// Avoid doing these twice
userOptions.xAxis = userOptions.yAxis = void 0;
const options = merge({
chart: {
panning: {
enabled: true,
type: 'x'
},
zooming: {
pinchType: 'x',
mouseWheel: {
type: 'x'
}
}
},
navigator: {
enabled: navigatorEnabled
},
scrollbar: {
// #4988 - check if setOptions was called
enabled: pick(ScrollbarDefaults.enabled, true)
},
rangeSelector: {
// #4988 - check if setOptions was called
enabled: pick(RangeSelectorDefaults.rangeSelector.enabled, true)
},
title: {
text: null
},
tooltip: {
split: pick(defaultOptions.tooltip?.split, true),
crosshairs: true
},
legend: {
enabled: false
}
}, userOptions, // User's options
{
isStock: true // Internal flag
});
userOptions.xAxis = xAxisOptions;
userOptions.yAxis = yAxisOptions;
// Apply X axis options to both single and multi y axes
options.xAxis = splat(userOptions.xAxis || {}).map((xAxisOptions) => merge(getDefaultAxisOptions('xAxis', xAxisOptions, defaultOptions.xAxis),
// #7690
xAxisOptions, // User options
getForcedAxisOptions('xAxis', userOptions)));
// Apply Y axis options to both single and multi y axes
options.yAxis = splat(userOptions.yAxis || {}).map((yAxisOptions) => merge(getDefaultAxisOptions('yAxis', yAxisOptions, defaultOptions.yAxis),
// #7690
yAxisOptions // User options
));
super.init(options, callback);
}
/**
* Factory for creating different axis types.
* Extended to add stock defaults.
*
* @private
* @function Highcharts.StockChart#createAxis
* @param {string} coll
* An axis type.
* @param {Chart.CreateAxisOptionsObject} options
* The axis creation options.
*/
createAxis(coll, options) {
options.axis = merge(getDefaultAxisOptions(coll, options.axis, getOptions()[coll]), options.axis, getForcedAxisOptions(coll, this.userOptions));
return super.createAxis(coll, options);
}
}
addEvent(Chart, 'update', function (e) {
const chart = this, options = e.options;
// Use case: enabling scrollbar from a disabled state.
// Scrollbar needs to be initialized from a controller, Navigator in this
// case (#6615)
if ('scrollbar' in options && chart.navigator) {
merge(true, chart.options.scrollbar, options.scrollbar);
chart.navigator.update({ enabled: !!chart.navigator.navigatorEnabled });
delete options.scrollbar;
}
});
/* *
*
* Composition
*
* */
(function (StockChart) {
/* *
*
* Functions
*
* */
/** @private */
function compose(ChartClass, AxisClass, SeriesClass, SVGRendererClass) {
const seriesProto = SeriesClass.prototype;
if (!seriesProto.forceCropping) {
addEvent(AxisClass, 'afterDrawCrosshair', onAxisAfterDrawCrosshair);
addEvent(AxisClass, 'afterHideCrosshair', onAxisAfterHideCrosshair);
addEvent(AxisClass, 'autoLabelAlign', onAxisAutoLabelAlign);
addEvent(AxisClass, 'destroy', onAxisDestroy);
addEvent(AxisClass, 'getPlotLinePath', onAxisGetPlotLinePath);
ChartClass.prototype.setFixedRange = setFixedRange;
seriesProto.forceCropping = seriesForceCropping;
addEvent(SeriesClass, 'setOptions', onSeriesSetOptions);
SVGRendererClass.prototype.crispPolyLine = svgRendererCrispPolyLine;
}
}
StockChart.compose = compose;
/**
* Extend crosshairs to also draw the label.
* @private
*/
function onAxisAfterDrawCrosshair(event) {
const axis = this;
// Check if the label has to be drawn
if (!(axis.crosshair?.label?.enabled &&
axis.cross &&
isNumber(axis.min) &&
isNumber(axis.max))) {
return;
}
const chart = axis.chart, log = axis.logarithmic, options = axis.crosshair.label, // The label's options
horiz = axis.horiz, // Axis orientation
opposite = axis.opposite, // Axis position
left = axis.left, // Left position
top = axis.top, // Top position
width = axis.width, tickInside = axis.options.tickPosition === 'inside', snap = axis.crosshair.snap !== false, e = event.e || (axis.cross?.e), point = event.point;
let crossLabel = axis.crossLabel, // The svgElement
posx, posy, formatOption = options.format, formatFormat = '', limit, offset = 0,
// Use last available event (#5287)
min = axis.min, max = axis.max;
if (log) {
min = log.lin2log(axis.min);
max = log.lin2log(axis.max);
}
const align = (horiz ? 'center' : opposite ?
(axis.labelAlign === 'right' ? 'right' : 'left') :
(axis.labelAlign === 'left' ? 'left' : 'center'));
// If the label does not exist yet, create it.
if (!crossLabel) {
crossLabel = axis.crossLabel = chart.renderer
.label('', 0, void 0, options.shape || 'callout')
.addClass('highcharts-crosshair-label highcharts-color-' + (point?.series ?
point.series.colorIndex :
axis.series[0] && this.series[0].colorIndex))
.attr({
align: options.align || align,
padding: pick(options.padding, 8),
r: pick(options.borderRadius, 3),
zIndex: 2
})
.add(axis.labelGroup);
// Presentational
if (!chart.styledMode) {
crossLabel
.attr({
fill: options.backgroundColor ||
point?.series?.color || // #14888
"#666666" /* Palette.neutralColor60 */,
stroke: options.borderColor || '',
'stroke-width': options.borderWidth || 0
})
.css(extend({
color: "#ffffff" /* Palette.backgroundColor */,
fontWeight: 'normal',
fontSize: '0.7em',
textAlign: 'center'
}, options.style || {}));
}
}
if (horiz) {
posx = snap ? (point.plotX || 0) + left : e.chartX;
posy = top + (opposite ? 0 : axis.height);
}
else {
posx = left + axis.offset + (opposite ? width : 0);
posy = snap ? (point.plotY || 0) + top : e.chartY;
}
if (!formatOption && !options.formatter) {
if (axis.dateTime) {
formatFormat = '%b %d, %Y';
}
formatOption =
'{value' + (formatFormat ? ':' + formatFormat : '') + '}';
}
// Show the label
const value = snap ?
(axis.isXAxis ? point.x : point.y) :
axis.toValue(horiz ? e.chartX : e.chartY);
// Crosshair should be rendered within Axis range (#7219) and the point
// of currentPriceIndicator should be inside the plot area (#14879).
const isInside = point?.series ?
point.series.isPointInside(point) :
(isNumber(value) && value > min && value < max);
let text = '';
if (formatOption) {
text = format(formatOption, { value }, chart);
}
else if (options.formatter && isNumber(value)) {
text = options.formatter.call(axis, value);
}
crossLabel.attr({
text,
x: posx,
y: posy,
visibility: isInside ? 'inherit' : 'hidden'
});
const crossBox = crossLabel.getBBox();
// Now it is placed we can correct its position
if (isNumber(crossLabel.x) && !horiz && !opposite) {
posx = crossLabel.x - (crossBox.width / 2);
}
if (isNumber(crossLabel.y)) {
if (horiz) {
if ((tickInside && !opposite) || (!tickInside && opposite)) {
posy = crossLabel.y - crossBox.height;
}
}
else {
posy = crossLabel.y - (crossBox.height / 2);
}
}
// Check the edges
if (horiz) {
limit = {
left,
right: left + axis.width
};
}
else {
limit = {
left: axis.labelAlign === 'left' ? left : 0,
right: axis.labelAlign === 'right' ?
left + axis.width :
chart.chartWidth
};
}
const translateX = crossLabel.translateX || 0;
// Left edge
if (translateX < limit.left) {
offset = limit.left - translateX;
}
// Right edge
if (translateX + crossBox.width >= limit.right) {
offset = -(translateX + crossBox.width - limit.right);
}
// Show the crosslabel
crossLabel.attr({
x: Math.max(0, posx + offset),
y: Math.max(0, posy),
// First set x and y, then anchorX and anchorY, when box is actually
// calculated, #5702
anchorX: horiz ?
posx :
(axis.opposite ? 0 : chart.chartWidth),
anchorY: horiz ?
(axis.opposite ? chart.chartHeight : 0) :
posy + crossBox.height / 2
});
}
/**
* Wrapper to hide the label.
* @private
*/
function onAxisAfterHideCrosshair() {
const axis = this;
if (axis.crossLabel) {
axis.crossLabel = axis.crossLabel.hide();
}
}
/**
* Override the automatic label alignment so that the first Y axis' labels
* are drawn on top of the grid line, and subsequent axes are drawn outside.
* @private
*/
function onAxisAutoLabelAlign(e) {
const axis = this, chart = axis.chart, options = axis.options, panes = chart._labelPanes = chart._labelPanes || {}, labelOptions = options.labels;
if (chart.options.isStock && axis.coll === 'yAxis') {
const key = options.top + ',' + options.height;
// Do it only for the first Y axis of each pane
if (!panes[key] && labelOptions.enabled) {
if (labelOptions.distance === 15 && // Default
axis.side === 1) {
labelOptions.distance = 0;
}
if (typeof labelOptions.align === 'undefined') {
labelOptions.align = 'right';
}
panes[key] = axis;
e.align = 'right';
e.preventDefault();
}
}
}
/**
* Clear axis from label panes. (#6071)
* @private
*/
function onAxisDestroy() {
const axis = this, chart = axis.chart, key = (axis.options &&
(axis.options.top + ',' + axis.options.height));
if (key && chart._labelPanes && chart._labelPanes[key] === axis) {
delete chart._labelPanes[key];
}
}
/**
* Override getPlotLinePath to allow for multipane charts.
* @private
*/
function onAxisGetPlotLinePath(e) {
const axis = this, series = (axis.isLinked && !axis.series && axis.linkedParent ?
axis.linkedParent.series :
axis.series), chart = axis.chart, renderer = chart.renderer, axisLeft = axis.left, axisTop = axis.top, result = [], translatedValue = e.translatedValue, value = e.value, force = e.force,
/**
* Return the other axis based on either the axis option or on
* related series.
* @private
*/
getAxis = (coll) => {
const otherColl = coll === 'xAxis' ? 'yAxis' : 'xAxis', opt = axis.options[otherColl];
// Other axis indexed by number
if (isNumber(opt)) {
return [chart[otherColl][opt]];
}
// Other axis indexed by id (like navigator)
if (isString(opt)) {
return [chart.get(opt)];
}
// Auto detect based on existing series
return series.map((s) => s[otherColl]);
};
let x1, y1, x2, y2, axes = [], // #3416 need a default array
axes2, uniqueAxes, transVal;
if ( // For stock chart, by default render paths across the panes
// except the case when `acrossPanes` is disabled by user (#6644)
(chart.options.isStock && e.acrossPanes !== false) &&
// Ignore in case of colorAxis or zAxis. #3360, #3524, #6720
axis.coll === 'xAxis' || axis.coll === 'yAxis') {
e.preventDefault();
// Get the related axes based on series
axes = getAxis(axis.coll);
// Get the related axes based options.*Axis setting #2810
axes2 = (axis.isXAxis ? chart.yAxis : chart.xAxis);
for (const A of axes2) {
if (!A.options.isInternal) {
const a = (A.isXAxis ? 'yAxis' : 'xAxis'), relatedAxis = (defined(A.options[a]) ?
chart[a][A.options[a]] :
chart[a][0]);
if (axis === relatedAxis) {
axes.push(A);
}
}
}
// Remove duplicates in the axes array. If there are no axes in the
// axes array, we are adding an axis without data, so we need to
// populate this with grid lines (#2796).
uniqueAxes = axes.length ?
[] :
[axis.isXAxis ? chart.yAxis[0] : chart.xAxis[0]]; // #3742
for (const axis2 of axes) {
if (uniqueAxes.indexOf(axis2) === -1 &&
// Do not draw on axis which overlap completely. #5424
!find(uniqueAxes, (unique) => (unique.pos === axis2.pos &&
unique.len === axis2.len))) {
uniqueAxes.push(axis2);
}
}
transVal = pick(translatedValue, axis.translate(value || 0, void 0, void 0, e.old));
if (isNumber(transVal)) {
if (axis.horiz) {
for (const axis2 of uniqueAxes) {
let skip;
y1 = axis2.pos;
y2 = y1 + axis2.len;
x1 = x2 = Math.round(transVal + axis.transB);
// Outside plot area
if (force !== 'pass' &&
(x1 < axisLeft || x1 > axisLeft + axis.width)) {
if (force) {
x1 = x2 = clamp(x1, axisLeft, axisLeft + axis.width);
}
else {
skip = true;
}
}
if (!skip) {
result.push(['M', x1, y1], ['L', x2, y2]);
}
}
}
else {
for (const axis2 of uniqueAxes) {
let skip;
x1 = axis2.pos;
x2 = x1 + axis2.len;
y1 = y2 = Math.round(axisTop + axis.height - transVal);
// Outside plot area
if (force !== 'pass' &&
(y1 < axisTop || y1 > axisTop + axis.height)) {
if (force) {
y1 = y2 = clamp(y1, axisTop, axisTop + axis.height);
}
else {
skip = true;
}
}
if (!skip) {
result.push(['M', x1, y1], ['L', x2, y2]);
}
}
}
}
e.path = result.length > 0 ?
renderer.crispPolyLine(result, e.lineWidth || 1) :
// #3557 getPlotLinePath in regular Highcharts also returns null
void 0;
}
}
/**
* Handle som Stock-specific series defaults, override the plotOptions
* before series options are handled.
* @private
*/
function onSeriesSetOptions(e) {
const series = this;
if (series.chart.options.isStock) {
let overrides;
if (series.is('column') || series.is('columnrange')) {
overrides = {
borderWidth: 0,
shadow: false
};
}
else if (!series.is('scatter') && !series.is('sma')) {
overrides = {
marker: {
enabled: false,
radius: 2
}
};
}
if (overrides) {
e.plotOptions[series.type] = merge(e.plotOptions[series.type], overrides);
}
}
}
/**
* Based on the data grouping options decides whether
* the data should be cropped while processing.
*
* @ignore
* @function Highcharts.Series#forceCropping
*/
function seriesForceCropping() {
const series = this, chart = series.chart, options = series.options, dataGroupingOptions = options.dataGrouping, groupingEnabled = (series.allowDG !== false &&
dataGroupingOptions &&
pick(dataGroupingOptions.enabled, chart.options.isStock));
return groupingEnabled;
}
/* eslint-disable jsdoc/check-param-names */
/**
* Factory function for creating new stock charts. Creates a new
* {@link Highcharts.StockChart|StockChart} object with different default
* options than the basic Chart.
*
* @example
* let chart = Highcharts.stockChart('container', {
* series: [{
* data: [1, 2, 3, 4, 5, 6, 7, 8, 9],
* pointInterval: 24 * 60 * 60 * 1000
* }]
* });
*
* @function Highcharts.stockChart
*
* @param {string|Highcharts.HTMLDOMElement} [renderTo]
* The DOM element to render to, or its id.
*
* @param {Highcharts.Options} options
* The chart options structure as described in the
* [options reference](https://api.highcharts.com/highstock).
*
* @param {Highcharts.ChartCallbackFunction} [callback]
* A function to execute when the chart object is finished
* rendering and all external image files (`chart.backgroundImage`,
* `chart.plotBackgroundImage` etc) are loaded. Defining a
* [chart.events.load](https://api.highcharts.com/highstock/chart.events.load)
* handler is equivalent.
*
* @return {Highcharts.StockChart}
* The chart object.
*/
function stockChart(a, b, c) {
return new StockChart(a, b, c);
}
StockChart.stockChart = stockChart;
/* eslint-enable jsdoc/check-param-names */
/**
* Function to crisp a line with multiple segments
*
* @private
* @function Highcharts.SVGRenderer#crispPolyLine
*/
function svgRendererCrispPolyLine(points, width) {
// Points format: [['M', 0, 0], ['L', 100, 0]]
// normalize to a crisp line
for (let i = 0; i < points.length; i = i + 2) {
const start = points[i], end = points[i + 1];
if (defined(start[1]) && start[1] === end[1]) {
start[1] = end[1] = crisp(start[1], width);
}
if (defined(start[2]) && start[2] === end[2]) {
start[2] = end[2] = crisp(start[2], width);
}
}
return points;
}
})(StockChart || (StockChart = {}));
/* *
*
* Default Export
*
* */
export default StockChart;