/* * * * (c) 2010-2024 Torstein Honsi * * License: www.highcharts.com/license * * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!! * * Highcharts feature to make the Y axis stay fixed when scrolling the chart * horizontally on mobile devices. Supports left and right side axes. */ 'use strict'; import A from '../Core/Animation/AnimationUtilities.js'; const { stop } = A; import H from '../Core/Globals.js'; const { composed } = H; import RendererRegistry from '../Core/Renderer/RendererRegistry.js'; import U from '../Core/Utilities.js'; const { addEvent, createElement, css, defined, merge, pushUnique } = U; /* * * * Functions * * */ /** @private */ function onChartRender() { let scrollablePlotArea = this.scrollablePlotArea; if ((this.scrollablePixelsX || this.scrollablePixelsY) && !scrollablePlotArea) { this.scrollablePlotArea = scrollablePlotArea = new ScrollablePlotArea(this); } scrollablePlotArea?.applyFixed(); } /** @private */ function markDirty() { if (this.chart.scrollablePlotArea) { this.chart.scrollablePlotArea.isDirty = true; } } class ScrollablePlotArea { static compose(AxisClass, ChartClass, SeriesClass) { if (pushUnique(composed, this.compose)) { addEvent(AxisClass, 'afterInit', markDirty); addEvent(ChartClass, 'afterSetChartSize', (e) => this.afterSetSize(e.target, e)); addEvent(ChartClass, 'render', onChartRender); addEvent(SeriesClass, 'show', markDirty); } } static afterSetSize(chart, e) { const { minWidth, minHeight } = chart.options.chart.scrollablePlotArea || {}, { clipBox, plotBox, inverted, renderer } = chart; let scrollablePixelsX, scrollablePixelsY, recalculateHoriz; if (!renderer.forExport) { // The amount of pixels to scroll, the difference between chart // width and scrollable width if (minWidth) { chart.scrollablePixelsX = scrollablePixelsX = Math.max(0, minWidth - chart.chartWidth); if (scrollablePixelsX) { chart.scrollablePlotBox = merge(chart.plotBox); plotBox.width = chart.plotWidth += scrollablePixelsX; clipBox[inverted ? 'height' : 'width'] += scrollablePixelsX; recalculateHoriz = true; } // Currently we can only do either X or Y } else if (minHeight) { chart.scrollablePixelsY = scrollablePixelsY = Math.max(0, minHeight - chart.chartHeight); if (defined(scrollablePixelsY)) { chart.scrollablePlotBox = merge(chart.plotBox); plotBox.height = chart.plotHeight += scrollablePixelsY; clipBox[inverted ? 'width' : 'height'] += scrollablePixelsY; recalculateHoriz = false; } } if (defined(recalculateHoriz) && !e.skipAxes) { for (const axis of chart.axes) { // Apply the corrected plot size to the axes of the other // orientation than the scrolling direction if (axis.horiz === recalculateHoriz) { axis.setAxisSize(); axis.setAxisTranslation(); } } } } } constructor(chart) { const chartOptions = chart.options.chart, Renderer = RendererRegistry.getRendererType(), scrollableOptions = chartOptions.scrollablePlotArea || {}, moveFixedElements = this.moveFixedElements.bind(this), styles = { WebkitOverflowScrolling: 'touch', overflowX: 'hidden', overflowY: 'hidden' }; if (chart.scrollablePixelsX) { styles.overflowX = 'auto'; } if (chart.scrollablePixelsY) { styles.overflowY = 'auto'; } this.chart = chart; // Insert a container with relative position that scrolling and fixed // container renders to (#10555) const parentDiv = this.parentDiv = createElement('div', { className: 'highcharts-scrolling-parent' }, { position: 'relative' }, chart.renderTo), // Add the necessary divs to provide scrolling scrollingContainer = this.scrollingContainer = createElement('div', { 'className': 'highcharts-scrolling' }, styles, parentDiv), innerContainer = this.innerContainer = createElement('div', { 'className': 'highcharts-inner-container' }, void 0, scrollingContainer), fixedDiv = this.fixedDiv = createElement('div', { className: 'highcharts-fixed' }, { position: 'absolute', overflow: 'hidden', pointerEvents: 'none', zIndex: (chartOptions.style?.zIndex || 0) + 2, top: 0 }, void 0, true), fixedRenderer = this.fixedRenderer = new Renderer(fixedDiv, chart.chartWidth, chart.chartHeight, chartOptions.style); // Mask this.mask = fixedRenderer .path() .attr({ fill: chartOptions.backgroundColor || '#fff', 'fill-opacity': scrollableOptions.opacity ?? 0.85, zIndex: -1 }) .addClass('highcharts-scrollable-mask') .add(); scrollingContainer.parentNode.insertBefore(fixedDiv, scrollingContainer); css(chart.renderTo, { overflow: 'visible' }); addEvent(chart, 'afterShowResetZoom', moveFixedElements); addEvent(chart, 'afterApplyDrilldown', moveFixedElements); addEvent(chart, 'afterLayOutTitles', moveFixedElements); // On scroll, reset the chart position because it applies to the // scrolled container let lastHoverPoint; addEvent(scrollingContainer, 'scroll', () => { const { pointer, hoverPoint } = chart; if (pointer) { delete pointer.chartPosition; if (hoverPoint) { lastHoverPoint = hoverPoint; } pointer.runPointActions(void 0, lastHoverPoint, true); } }); // Now move the container inside innerContainer.appendChild(chart.container); } applyFixed() { const { chart, fixedRenderer, isDirty, scrollingContainer } = this, { axisOffset, chartWidth, chartHeight, container, plotHeight, plotLeft, plotTop, plotWidth, scrollablePixelsX = 0, scrollablePixelsY = 0 } = chart, chartOptions = chart.options.chart, scrollableOptions = chartOptions.scrollablePlotArea || {}, { scrollPositionX = 0, scrollPositionY = 0 } = scrollableOptions, scrollableWidth = chartWidth + scrollablePixelsX, scrollableHeight = chartHeight + scrollablePixelsY; // Set the size of the fixed renderer to the visible width fixedRenderer.setSize(chartWidth, chartHeight); if (isDirty ?? true) { this.isDirty = false; this.moveFixedElements(); } // Increase the size of the scrollable renderer and background stop(chart.container); css(container, { width: `${scrollableWidth}px`, height: `${scrollableHeight}px` }); chart.renderer.boxWrapper.attr({ width: scrollableWidth, height: scrollableHeight, viewBox: [0, 0, scrollableWidth, scrollableHeight].join(' ') }); chart.chartBackground?.attr({ width: scrollableWidth, height: scrollableHeight }); css(scrollingContainer, { width: `${chartWidth}px`, height: `${chartHeight}px` }); // Set scroll position the first time (this.isDirty was undefined at // the top of this function) if (!defined(isDirty)) { scrollingContainer.scrollLeft = scrollablePixelsX * scrollPositionX; scrollingContainer.scrollTop = scrollablePixelsY * scrollPositionY; } // Mask behind the left and right side const maskTop = plotTop - axisOffset[0] - 1, maskLeft = plotLeft - axisOffset[3] - 1, maskBottom = plotTop + plotHeight + axisOffset[2] + 1, maskRight = plotLeft + plotWidth + axisOffset[1] + 1, maskPlotRight = plotLeft + plotWidth - scrollablePixelsX, maskPlotBottom = plotTop + plotHeight - scrollablePixelsY; let d = [['M', 0, 0]]; if (scrollablePixelsX) { d = [ // Left side ['M', 0, maskTop], ['L', plotLeft - 1, maskTop], ['L', plotLeft - 1, maskBottom], ['L', 0, maskBottom], ['Z'], // Right side ['M', maskPlotRight, maskTop], ['L', chartWidth, maskTop], ['L', chartWidth, maskBottom], ['L', maskPlotRight, maskBottom], ['Z'] ]; } else if (scrollablePixelsY) { d = [ // Top side ['M', maskLeft, 0], ['L', maskLeft, plotTop - 1], ['L', maskRight, plotTop - 1], ['L', maskRight, 0], ['Z'], // Bottom side ['M', maskLeft, maskPlotBottom], ['L', maskLeft, chartHeight], ['L', maskRight, chartHeight], ['L', maskRight, maskPlotBottom], ['Z'] ]; } if (chart.redrawTrigger !== 'adjustHeight') { this.mask.attr({ d }); } } /** * These elements are moved over to the fixed renderer and stay fixed when * the user scrolls the chart * @private */ moveFixedElements() { const { container, inverted, scrollablePixelsX, scrollablePixelsY } = this.chart, fixedRenderer = this.fixedRenderer, fixedSelectors = [ '.highcharts-breadcrumbs-group', '.highcharts-contextbutton', '.highcharts-caption', '.highcharts-credits', '.highcharts-legend', '.highcharts-legend-checkbox', '.highcharts-navigator-series', '.highcharts-navigator-xaxis', '.highcharts-navigator-yaxis', '.highcharts-navigator', '.highcharts-reset-zoom', '.highcharts-drillup-button', '.highcharts-scrollbar', '.highcharts-subtitle', '.highcharts-title' ]; let axisClass; if (scrollablePixelsX && !inverted) { axisClass = '.highcharts-yaxis'; } else if (scrollablePixelsX && inverted) { axisClass = '.highcharts-xaxis'; } else if (scrollablePixelsY && !inverted) { axisClass = '.highcharts-xaxis'; } else if (scrollablePixelsY && inverted) { axisClass = '.highcharts-yaxis'; } if (axisClass) { fixedSelectors.push(`${axisClass}:not(.highcharts-radial-axis)`, `${axisClass}-labels:not(.highcharts-radial-axis-labels)`); } for (const className of fixedSelectors) { [].forEach.call(container.querySelectorAll(className), (elem) => { (elem.namespaceURI === fixedRenderer.SVG_NS ? fixedRenderer.box : fixedRenderer.box.parentNode).appendChild(elem); elem.style.pointerEvents = 'auto'; }); } } } /* * * * Default Export * * */ export default ScrollablePlotArea; /* * * * API Declarations * * */ /** * Options for a scrollable plot area. This feature provides a minimum size for * the plot area of the chart. If the size gets smaller than this, typically * on mobile devices, a native browser scrollbar is presented. This scrollbar * provides smooth scrolling for the contents of the plot area, whereas the * title, legend and unaffected axes are fixed. * * Since v7.1.2, a scrollable plot area can be defined for either horizontal or * vertical scrolling, depending on whether the `minWidth` or `minHeight` * option is set. * * @sample highcharts/chart/scrollable-plotarea * Scrollable plot area * @sample highcharts/chart/scrollable-plotarea-vertical * Vertically scrollable plot area * @sample {gantt} gantt/chart/scrollable-plotarea-vertical * Gantt chart with vertically scrollable plot area * * @since 6.1.0 * @product highcharts gantt * @apioption chart.scrollablePlotArea */ /** * The minimum height for the plot area. If it gets smaller than this, the plot * area will become scrollable. * * @type {number} * @since 7.1.2 * @apioption chart.scrollablePlotArea.minHeight */ /** * The minimum width for the plot area. If it gets smaller than this, the plot * area will become scrollable. * * @type {number} * @since 6.1.0 * @apioption chart.scrollablePlotArea.minWidth */ /** * The initial scrolling position of the scrollable plot area. Ranges from 0 to * 1, where 0 aligns the plot area to the left and 1 aligns it to the right. * Typically we would use 1 if the chart has right aligned Y axes. * * @type {number} * @since 6.1.0 * @apioption chart.scrollablePlotArea.scrollPositionX */ /** * The initial scrolling position of the scrollable plot area. Ranges from 0 to * 1, where 0 aligns the plot area to the top and 1 aligns it to the bottom. * * @type {number} * @since 7.1.2 * @apioption chart.scrollablePlotArea.scrollPositionY */ /** * The opacity of mask applied on one of the sides of the plot * area. * * @sample {highcharts} highcharts/chart/scrollable-plotarea-opacity * Disabled opacity for the mask * * @type {number} * @default 0.85 * @since 7.1.1 * @apioption chart.scrollablePlotArea.opacity */ (''); // Keep doclets above in transpiled file