highcharts
Version:
JavaScript charting framework
659 lines (658 loc) • 22.8 kB
JavaScript
/* *
*
* (c) 2010-2025 Torstein Honsi
*
* License: www.highcharts.com/license
*
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
*
* */
'use strict';
import D from '../../Core/Defaults.js';
const { defaultOptions } = D;
import H from '../../Core/Globals.js';
const { composed } = H;
import ScrollbarAxis from '../../Core/Axis/ScrollbarAxis.js';
import ScrollbarDefaults from './ScrollbarDefaults.js';
import U from '../../Core/Utilities.js';
const { addEvent, correctFloat, crisp, defined, destroyObjectProperties, extend, fireEvent, merge, pick, pushUnique, removeEvent } = U;
/* *
*
* Constants
*
* */
/* eslint-disable no-invalid-this, valid-jsdoc */
/**
* A reusable scrollbar, internally used in Highcharts Stock's
* navigator and optionally on individual axes.
*
* @private
* @class
* @name Highcharts.Scrollbar
* @param {Highcharts.SVGRenderer} renderer
* @param {Highcharts.ScrollbarOptions} options
* @param {Highcharts.Chart} chart
*/
class Scrollbar {
/* *
*
* Static Functions
*
* */
static compose(AxisClass) {
ScrollbarAxis.compose(AxisClass, Scrollbar);
if (pushUnique(composed, 'Scrollbar')) {
extend(defaultOptions, { scrollbar: ScrollbarDefaults });
}
}
/**
* When we have vertical scrollbar, rifles and arrow in buttons should be
* rotated. The same method is used in Navigator's handles, to rotate them.
*
* @function Highcharts.swapXY
*
* @param {Highcharts.SVGPathArray} path
* Path to be rotated.
*
* @param {boolean} [vertical]
* If vertical scrollbar, swap x-y values.
*
* @return {Highcharts.SVGPathArray}
* Rotated path.
*
* @requires modules/stock
*/
static swapXY(path, vertical) {
if (vertical) {
path.forEach((seg) => {
const len = seg.length;
let temp;
for (let i = 0; i < len; i += 2) {
temp = seg[i + 1];
if (typeof temp === 'number') {
seg[i + 1] = seg[i + 2];
seg[i + 2] = temp;
}
}
});
}
return path;
}
/* *
*
* Constructors
*
* */
constructor(renderer, options, chart) {
/* *
*
* Properties
*
* */
this._events = [];
this.chartX = 0;
this.chartY = 0;
this.from = 0;
this.scrollbarButtons = [];
this.scrollbarLeft = 0;
this.scrollbarStrokeWidth = 1;
this.scrollbarTop = 0;
this.size = 0;
this.to = 0;
this.trackBorderWidth = 1;
this.x = 0;
this.y = 0;
this.init(renderer, options, chart);
}
/* *
*
* Functions
*
* */
/**
* Set up the mouse and touch events for the Scrollbar
*
* @private
* @function Highcharts.Scrollbar#addEvents
*/
addEvents() {
const buttonsOrder = this.options.inverted ? [1, 0] : [0, 1], buttons = this.scrollbarButtons, bar = this.scrollbarGroup.element, track = this.track.element, mouseDownHandler = this.mouseDownHandler.bind(this), mouseMoveHandler = this.mouseMoveHandler.bind(this), mouseUpHandler = this.mouseUpHandler.bind(this);
const _events = [
// Mouse events
[
buttons[buttonsOrder[0]].element,
'click',
this.buttonToMinClick.bind(this)
],
[
buttons[buttonsOrder[1]].element,
'click',
this.buttonToMaxClick.bind(this)
],
[track, 'click', this.trackClick.bind(this)],
[bar, 'mousedown', mouseDownHandler],
[bar.ownerDocument, 'mousemove', mouseMoveHandler],
[bar.ownerDocument, 'mouseup', mouseUpHandler],
// Touch events
[bar, 'touchstart', mouseDownHandler],
[bar.ownerDocument, 'touchmove', mouseMoveHandler],
[bar.ownerDocument, 'touchend', mouseUpHandler]
];
// Add them all
_events.forEach(function (args) {
addEvent.apply(null, args);
});
this._events = _events;
}
buttonToMaxClick(e) {
const scroller = this;
const range = ((scroller.to - scroller.from) *
pick(scroller.options.step, 0.2));
scroller.updatePosition(scroller.from + range, scroller.to + range);
fireEvent(scroller, 'changed', {
from: scroller.from,
to: scroller.to,
trigger: 'scrollbar',
DOMEvent: e
});
}
buttonToMinClick(e) {
const scroller = this;
const range = correctFloat(scroller.to - scroller.from) *
pick(scroller.options.step, 0.2);
scroller.updatePosition(correctFloat(scroller.from - range), correctFloat(scroller.to - range));
fireEvent(scroller, 'changed', {
from: scroller.from,
to: scroller.to,
trigger: 'scrollbar',
DOMEvent: e
});
}
/**
* Get normalized (0-1) cursor position over the scrollbar
*
* @private
* @function Highcharts.Scrollbar#cursorToScrollbarPosition
*
* @param {*} normalizedEvent
* normalized event, with chartX and chartY values
*
* @return {Highcharts.Dictionary<number>}
* Local position {chartX, chartY}
*/
cursorToScrollbarPosition(normalizedEvent) {
const scroller = this, options = scroller.options, minWidthDifference = options.minWidth > scroller.calculatedWidth ?
options.minWidth :
0; // `minWidth` distorts translation
return {
chartX: (normalizedEvent.chartX - scroller.x -
scroller.xOffset) /
(scroller.barWidth - minWidthDifference),
chartY: (normalizedEvent.chartY - scroller.y -
scroller.yOffset) /
(scroller.barWidth - minWidthDifference)
};
}
/**
* Destroys allocated elements.
*
* @private
* @function Highcharts.Scrollbar#destroy
*/
destroy() {
const scroller = this, navigator = scroller.chart.scroller;
// Disconnect events added in addEvents
scroller.removeEvents();
// Destroy properties
[
'track',
'scrollbarRifles',
'scrollbar',
'scrollbarGroup',
'group'
].forEach(function (prop) {
if (scroller[prop] && scroller[prop].destroy) {
scroller[prop] = scroller[prop].destroy();
}
});
// #6421, chart may have more scrollbars
if (navigator && scroller === navigator.scrollbar) {
navigator.scrollbar = null;
// Destroy elements in collection
destroyObjectProperties(navigator.scrollbarButtons);
}
}
/**
* Draw the scrollbar buttons with arrows
*
* @private
* @function Highcharts.Scrollbar#drawScrollbarButton
* @param {number} index
* 0 is left, 1 is right
*/
drawScrollbarButton(index) {
const scroller = this, renderer = scroller.renderer, scrollbarButtons = scroller.scrollbarButtons, options = scroller.options, size = scroller.size, group = renderer.g().add(scroller.group);
scrollbarButtons.push(group);
if (options.buttonsEnabled) {
// Create a rectangle for the scrollbar button
const rect = renderer.rect()
.addClass('highcharts-scrollbar-button')
.add(group);
// Presentational attributes
if (!scroller.chart.styledMode) {
rect.attr({
stroke: options.buttonBorderColor,
'stroke-width': options.buttonBorderWidth,
fill: options.buttonBackgroundColor
});
}
// Place the rectangle based on the rendered stroke width
rect.attr(rect.crisp({
x: -0.5,
y: -0.5,
width: size,
height: size,
r: options.buttonBorderRadius
}, rect.strokeWidth()));
// Button arrow
const arrow = renderer
.path(Scrollbar.swapXY([[
'M',
size / 2 + (index ? -1 : 1),
size / 2 - 3
], [
'L',
size / 2 + (index ? -1 : 1),
size / 2 + 3
], [
'L',
size / 2 + (index ? 2 : -2),
size / 2
]], options.vertical))
.addClass('highcharts-scrollbar-arrow')
.add(scrollbarButtons[index]);
if (!scroller.chart.styledMode) {
arrow.attr({
fill: options.buttonArrowColor
});
}
}
}
/**
* @private
* @function Highcharts.Scrollbar#init
* @param {Highcharts.SVGRenderer} renderer
* @param {Highcharts.ScrollbarOptions} options
* @param {Highcharts.Chart} chart
*/
init(renderer, options, chart) {
const scroller = this;
scroller.scrollbarButtons = [];
scroller.renderer = renderer;
scroller.userOptions = options;
scroller.options = merge(ScrollbarDefaults, defaultOptions.scrollbar, options);
scroller.options.margin = pick(scroller.options.margin, 10);
scroller.chart = chart;
// Backward compatibility
scroller.size = pick(scroller.options.size, scroller.options.height);
// Init
if (options.enabled) {
scroller.render();
scroller.addEvents();
}
}
mouseDownHandler(e) {
const scroller = this, normalizedEvent = scroller.chart.pointer?.normalize(e) || e, mousePosition = scroller.cursorToScrollbarPosition(normalizedEvent);
scroller.chartX = mousePosition.chartX;
scroller.chartY = mousePosition.chartY;
scroller.initPositions = [scroller.from, scroller.to];
scroller.grabbedCenter = true;
}
/**
* Event handler for the mouse move event.
* @private
*/
mouseMoveHandler(e) {
const scroller = this, normalizedEvent = scroller.chart.pointer?.normalize(e) || e, options = scroller.options, direction = options.vertical ?
'chartY' : 'chartX', initPositions = scroller.initPositions || [];
let scrollPosition, chartPosition, change;
// In iOS, a mousemove event with e.pageX === 0 is fired when
// holding the finger down in the center of the scrollbar. This
// should be ignored.
if (scroller.grabbedCenter &&
// #4696, scrollbar failed on Android
(!e.touches || e.touches[0][direction] !== 0)) {
chartPosition = scroller.cursorToScrollbarPosition(normalizedEvent)[direction];
scrollPosition = scroller[direction];
change = chartPosition - scrollPosition;
scroller.hasDragged = true;
scroller.updatePosition(initPositions[0] + change, initPositions[1] + change);
if (scroller.hasDragged) {
fireEvent(scroller, 'changed', {
from: scroller.from,
to: scroller.to,
trigger: 'scrollbar',
DOMType: e.type,
DOMEvent: e
});
}
}
}
/**
* Event handler for the mouse up event.
* @private
*/
mouseUpHandler(e) {
const scroller = this;
if (scroller.hasDragged) {
fireEvent(scroller, 'changed', {
from: scroller.from,
to: scroller.to,
trigger: 'scrollbar',
DOMType: e.type,
DOMEvent: e
});
}
scroller.grabbedCenter =
scroller.hasDragged =
scroller.chartX =
scroller.chartY = null;
}
/**
* Position the scrollbar, method called from a parent with defined
* dimensions.
*
* @private
* @function Highcharts.Scrollbar#position
* @param {number} x
* x-position on the chart
* @param {number} y
* y-position on the chart
* @param {number} width
* width of the scrollbar
* @param {number} height
* height of the scrollbar
*/
position(x, y, width, height) {
const scroller = this, options = scroller.options, { buttonsEnabled, margin = 0, vertical } = options, method = scroller.rendered ? 'animate' : 'attr';
let xOffset = height, yOffset = 0;
// Make the scrollbar visible when it is repositioned, #15763.
scroller.group.show();
scroller.x = x;
scroller.y = y + this.trackBorderWidth;
scroller.width = width; // Width with buttons
scroller.height = height;
scroller.xOffset = xOffset;
scroller.yOffset = yOffset;
// If Scrollbar is a vertical type, swap options:
if (vertical) {
scroller.width = scroller.yOffset = width = yOffset = scroller.size;
scroller.xOffset = xOffset = 0;
scroller.yOffset = yOffset = buttonsEnabled ? scroller.size : 0;
// Width without buttons
scroller.barWidth = height - (buttonsEnabled ? width * 2 : 0);
scroller.x = x = x + margin;
}
else {
scroller.height = height = scroller.size;
scroller.xOffset = xOffset = buttonsEnabled ? scroller.size : 0;
// Width without buttons
scroller.barWidth = width - (buttonsEnabled ? height * 2 : 0);
scroller.y = scroller.y + margin;
}
// Set general position for a group:
scroller.group[method]({
translateX: x,
translateY: scroller.y
});
// Resize background/track:
scroller.track[method]({
width: width,
height: height
});
// Move right/bottom button to its place:
scroller.scrollbarButtons[1][method]({
translateX: vertical ? 0 : width - xOffset,
translateY: vertical ? height - yOffset : 0
});
}
/**
* Removes the event handlers attached previously with addEvents.
*
* @private
* @function Highcharts.Scrollbar#removeEvents
*/
removeEvents() {
this._events.forEach(function (args) {
removeEvent.apply(null, args);
});
this._events.length = 0;
}
/**
* Render scrollbar with all required items.
*
* @private
* @function Highcharts.Scrollbar#render
*/
render() {
const scroller = this, renderer = scroller.renderer, options = scroller.options, size = scroller.size, styledMode = scroller.chart.styledMode, group = renderer.g('scrollbar')
.attr({
zIndex: options.zIndex
})
.hide() // Initially hide the scrollbar #15863
.add();
// Draw the scrollbar group
scroller.group = group;
// Draw the scrollbar track:
scroller.track = renderer.rect()
.addClass('highcharts-scrollbar-track')
.attr({
r: options.trackBorderRadius || 0,
height: size,
width: size
}).add(group);
if (!styledMode) {
scroller.track.attr({
fill: options.trackBackgroundColor,
stroke: options.trackBorderColor,
'stroke-width': options.trackBorderWidth
});
}
const trackBorderWidth = scroller.trackBorderWidth =
scroller.track.strokeWidth();
scroller.track.attr({
x: -crisp(0, trackBorderWidth),
y: -crisp(0, trackBorderWidth)
});
// Draw the scrollbar itself
scroller.scrollbarGroup = renderer.g().add(group);
scroller.scrollbar = renderer.rect()
.addClass('highcharts-scrollbar-thumb')
.attr({
height: size - trackBorderWidth,
width: size - trackBorderWidth,
r: options.barBorderRadius || 0
}).add(scroller.scrollbarGroup);
scroller.scrollbarRifles = renderer
.path(Scrollbar.swapXY([
['M', -3, size / 4],
['L', -3, 2 * size / 3],
['M', 0, size / 4],
['L', 0, 2 * size / 3],
['M', 3, size / 4],
['L', 3, 2 * size / 3]
], options.vertical))
.addClass('highcharts-scrollbar-rifles')
.add(scroller.scrollbarGroup);
if (!styledMode) {
scroller.scrollbar.attr({
fill: options.barBackgroundColor,
stroke: options.barBorderColor,
'stroke-width': options.barBorderWidth
});
scroller.scrollbarRifles.attr({
stroke: options.rifleColor,
'stroke-width': 1
});
}
scroller.scrollbarStrokeWidth = scroller.scrollbar.strokeWidth();
scroller.scrollbarGroup.translate(-crisp(0, scroller.scrollbarStrokeWidth), -crisp(0, scroller.scrollbarStrokeWidth));
// Draw the buttons:
scroller.drawScrollbarButton(0);
scroller.drawScrollbarButton(1);
}
/**
* Set scrollbar size, with a given scale.
*
* @private
* @function Highcharts.Scrollbar#setRange
* @param {number} from
* scale (0-1) where bar should start
* @param {number} to
* scale (0-1) where bar should end
*/
setRange(from, to) {
const scroller = this, options = scroller.options, vertical = options.vertical, minWidth = options.minWidth, fullWidth = scroller.barWidth, method = (this.rendered &&
!this.hasDragged &&
!(this.chart.navigator && this.chart.navigator.hasDragged)) ? 'animate' : 'attr';
if (!defined(fullWidth)) {
return;
}
const toPX = fullWidth * Math.min(to, 1);
let fromPX, newSize;
from = Math.max(from, 0);
fromPX = Math.ceil(fullWidth * from);
scroller.calculatedWidth = newSize = correctFloat(toPX - fromPX);
// We need to recalculate position, if minWidth is used
if (newSize < minWidth) {
fromPX = (fullWidth - minWidth + newSize) * from;
newSize = minWidth;
}
const newPos = Math.floor(fromPX + scroller.xOffset + scroller.yOffset);
const newRiflesPos = newSize / 2 - 0.5; // -0.5 -> rifle line width / 2
// Store current position:
scroller.from = from;
scroller.to = to;
if (!vertical) {
scroller.scrollbarGroup[method]({
translateX: newPos
});
scroller.scrollbar[method]({
width: newSize
});
scroller.scrollbarRifles[method]({
translateX: newRiflesPos
});
scroller.scrollbarLeft = newPos;
scroller.scrollbarTop = 0;
}
else {
scroller.scrollbarGroup[method]({
translateY: newPos
});
scroller.scrollbar[method]({
height: newSize
});
scroller.scrollbarRifles[method]({
translateY: newRiflesPos
});
scroller.scrollbarTop = newPos;
scroller.scrollbarLeft = 0;
}
if (newSize <= 12) {
scroller.scrollbarRifles.hide();
}
else {
scroller.scrollbarRifles.show();
}
// Show or hide the scrollbar based on the showFull setting
if (options.showFull === false) {
if (from <= 0 && to >= 1) {
scroller.group.hide();
}
else {
scroller.group.show();
}
}
scroller.rendered = true;
}
/**
* Checks if the extremes should be updated in response to a scrollbar
* change event.
*
* @private
* @function Highcharts.Scrollbar#shouldUpdateExtremes
*/
shouldUpdateExtremes(eventType) {
return (pick(this.options.liveRedraw, H.svg &&
!H.isTouchDevice &&
!this.chart.boosted) ||
// Mouseup always should change extremes
eventType === 'mouseup' ||
eventType === 'touchend' ||
// Internal events
!defined(eventType));
}
trackClick(e) {
const scroller = this;
const normalizedEvent = scroller.chart.pointer?.normalize(e) || e, range = scroller.to - scroller.from, top = scroller.y + scroller.scrollbarTop, left = scroller.x + scroller.scrollbarLeft;
if ((scroller.options.vertical && normalizedEvent.chartY > top) ||
(!scroller.options.vertical && normalizedEvent.chartX > left)) {
// On the top or on the left side of the track:
scroller.updatePosition(scroller.from + range, scroller.to + range);
}
else {
// On the bottom or the right side of the track:
scroller.updatePosition(scroller.from - range, scroller.to - range);
}
fireEvent(scroller, 'changed', {
from: scroller.from,
to: scroller.to,
trigger: 'scrollbar',
DOMEvent: e
});
}
/**
* Update the scrollbar with new options
*
* @private
* @function Highcharts.Scrollbar#update
* @param {Highcharts.ScrollbarOptions} options
*/
update(options) {
this.destroy();
this.init(this.chart.renderer, merge(true, this.options, options), this.chart);
}
/**
* Update position option in the Scrollbar, with normalized 0-1 scale
*
* @private
* @function Highcharts.Scrollbar#updatePosition
* @param {number} from
* @param {number} to
*/
updatePosition(from, to) {
if (to > 1) {
from = correctFloat(1 - correctFloat(to - from));
to = 1;
}
if (from < 0) {
to = correctFloat(to - from);
from = 0;
}
this.from = from;
this.to = to;
}
}
/* *
*
* Static Properties
*
* */
Scrollbar.defaultOptions = ScrollbarDefaults;
/* *
*
* Default Export
*
* */
export default Scrollbar;