/** * The Navigator component is used to visually set the visible range of the x-axis * of a cartesian chart. * * This component is meant to be used with the Navigator Container * via its {@link Ext.chart.navigator.Container#navigator} config. * * IMPORTANT: even though the Navigator component is a kind of chart, it should not be * treated as such. Correct behavior is not guaranteed when using hidden/private configs. */Ext.define('Ext.chart.navigator.Navigator', { extend: 'Ext.chart.navigator.NavigatorBase', isNavigator: true, requires: [ 'Ext.chart.navigator.sprite.RangeMask' ], config: { /** * @cfg {'bottom'/'top'} [docked='bottom'] */ docked: 'bottom', /** * @cfg {'series'/'chart'} [span='series'] * Whether the navigator should span the 'series' (default) or the whole 'chart'. */ span: 'series', insetPadding: 0, innerPadding: 0, /** * @cfg {Ext.chart.navigator.Container} navigatorContainer * 'parent' is reserved in Modern, 'container' is reserved in Classic, * so we use 'navigatorContainer' as a config name. * @private */ navigatorContainer: null, /** * @cfg {String} axis (required) * The ID of the {@link #chart chart's} axis to link to. * The axis should be positioned to 'bottom' or 'top' in the chart. */ axis: null, /** * @cfg {Number} [tolerance=20] * The maximum horizontal delta between the pointer/finger and the center of a navigator * thumb. Used for hit testing. */ tolerance: 20, /** * @cfg {Number} [minimum=0.8] * The start of the visible range, where the visible range is a [0, 1] interval. */ minimum: 0.8, /** * @cfg {Number} [maximum=1] * The end of the visible range, where the visible range is a [0, 1] interval. */ maximum: 1, /** * @cfg {Number} [thumbGap=30] * Minimum gap between navigator thumbs in pixels. */ thumbGap: 30, autoHideThumbs: true, width: '100%', /** * @cfg {Number} [height=75] * The height of the navigator component. */ height: 75 /** * @cfg flipXY * @hide */ /** * @cfg series * @hide */ /** * @cfg axes * @hide */ /** * @cfg store * @hide */ /** * @cfg legend * @hide */ /** * @cfg interactions * @hide */ /** * @cfg highlightItem * @hide */ /** * @cfg theme * @hide */ /** * @cfg innerPadding * @hide */ /** * @cfg insetPadding * @hide */ }, dragType: null, constructor: function(config) { var me = this, visibleRange, overlay; config = config || {}; visibleRange = [ config.minimum || 0.8, config.maximum || 1 ]; me.callParent([config]); overlay = me.overlaySurface; overlay.element.setStyle({ zIndex: 100 }); me.rangeMask = overlay.add({ type: 'rangemask', min: visibleRange[0], max: visibleRange[1], fillStyle: 'rgba(0, 0, 0, .25)' }); me.onDragEnd(); // Set 'thumbOpacity' of the range mask sprite to 0, if needed, // and apply animation modifier changes after that, so that the attribute is set // instantly. me.rangeMask.setAnimation({ duration: 500, customDurations: { min: 0, max: 0, translationX: 0, translationY: 0, scalingX: 0, scalingY: 0, scalingCenterX: 0, scalingCenterY: 0, fillStyle: 0, strokeStyle: 0 } }); me.setVisibleRange(visibleRange); }, createSurface: function(id) { var surface = this.callParent([id]); if (id === 'overlay') { this.overlaySurface = surface; } return surface; }, // Note: 'applyDock' and 'updateDock' won't ever be called in Classic. // See Classic NavigatorBase. applyAxis: function(axis) { return this.getNavigatorContainer().getChart().getAxis(axis); }, updateAxis: function(axis, oldAxis) { var me = this, eventName = 'visiblerangechange', eventHandler = 'onAxisVisibleRangeChange'; if (oldAxis) { oldAxis.un(eventName, eventHandler, me); } if (axis) { axis.on(eventName, eventHandler, me); } me.axis = axis; }, getAxis: function() { // The superclass doesn't have the 'axis' config, but it has the same method, // which we override here to act as a getter for the config. The user is not // expected to use the original method in this subclass anyway. return this.axis; }, onAxisVisibleRangeChange: function(axis, visibleRange) { this.setVisibleRange(visibleRange); }, updateNavigatorContainer: function(navigatorContainer) { var me = this, oldChart = me.chart, chart = me.chart = navigatorContainer && navigatorContainer.getChart(), chartSeriesList = chart && chart.getSeries(), // 'legendStore' already exists in the base class. chartLegendStore = me.chartLegendStore, navigatorSeriesList = [], storeEventName = 'update', // 'onLegendStoreUpdate' already exists in the base class. storeEventHandler = 'onChartLegendStoreUpdate', chartSeries, navigatorSeries, seriesConfig, i; if (oldChart) { oldChart.un('layout', 'afterBoundChartLayout', me); oldChart.un('themechange', 'onChartThemeChange', me); oldChart.un('storechange', 'onChartStoreChange', me); } chart.on('layout', 'afterBoundChartLayout', me); for (i = 0; i < chartSeriesList.length; i++) { chartSeries = chartSeriesList[i]; seriesConfig = me.getSeriesConfig(chartSeries); navigatorSeries = Ext.create('series.' + seriesConfig.type, seriesConfig); navigatorSeries.parentSeries = chartSeries; chartSeries.navigatorSeries = navigatorSeries; navigatorSeriesList.push(navigatorSeries); } if (chartLegendStore) { chartLegendStore.un(storeEventName, storeEventHandler, me); me.chartLegendStore = null; } if (chart) { me.setStore(chart.getStore()); me.chartLegendStore = chartLegendStore = chart.getLegendStore(); if (chartLegendStore) { chartLegendStore.on(storeEventName, storeEventHandler, me); } chart.on('themechange', 'onChartThemeChange', me); chart.on('storechange', 'onChartStoreChange', me); me.onChartThemeChange(chart, chart.getTheme()); } me.setSeries(navigatorSeriesList); }, onChartThemeChange: function(chart, theme) { this.setTheme(theme); }, onChartStoreChange: function(chart, store) { this.setStore(store); }, addCustomStyle: function(config, style, subStyle) { var fillStyle, strokeStyle; style = style || {}; subStyle = subStyle || {}; config.style = config.style || {}; config.subStyle = config.subStyle || {}; fillStyle = style && (style.fillStyle || style.fill); strokeStyle = style && (style.strokeStyle || style.stroke); if (fillStyle) { config.style.fillStyle = fillStyle; } if (strokeStyle) { config.style.strokeStyle = strokeStyle; } fillStyle = subStyle && (subStyle.fillStyle || subStyle.fill); strokeStyle = subStyle && (subStyle.strokeStyle || subStyle.stroke); if (fillStyle) { config.subStyle.fillStyle = fillStyle; } if (strokeStyle) { config.subStyle.strokeStyle = strokeStyle; } return config; }, getSeriesConfig: function(chartSeries) { var me = this, style = chartSeries.getStyle(), config; if (chartSeries.isLine) { config = me.addCustomStyle({ type: 'line', fill: true, xField: chartSeries.getXField(), yField: chartSeries.getYField(), smooth: chartSeries.getSmooth() }, style); } else if (chartSeries.isCandleStick) { config = me.addCustomStyle({ type: 'line', fill: true, xField: chartSeries.getXField(), yField: chartSeries.getCloseField() }, style.raiseStyle); } else if (chartSeries.isArea || chartSeries.isBar) { config = me.addCustomStyle({ type: 'area', xField: chartSeries.getXField(), yField: chartSeries.getYField() }, style, chartSeries.getSubStyle()); } else { Ext.raise("Navigator only works with 'line', 'bar', 'candlestick' and 'area' series."); } config.style.fillOpacity = 0.2; return config; }, onChartLegendStoreUpdate: function(store, record) { var me = this, chart = me.chart, series; if (chart && record) { series = chart.getSeries().map[record.get('series')]; if (series && series.navigatorSeries) { series.navigatorSeries.setHiddenByIndex(record.get('index'), record.get('disabled')); me.redraw(); } } }, setupEvents: function() { // Called from NavigatorBase classes. var me = this, overlayEl = me.overlaySurface.element; overlayEl.on({ scope: me, drag: 'onDrag', dragstart: 'onDragStart', dragend: 'onDragEnd', dragcancel: 'onDragEnd', mousemove: 'onMouseMove' }); }, onMouseMove: function(e) { var me = this, overlayEl = me.overlaySurface.element, style = overlayEl.dom.style, dragType = me.getDragType(e.pageX - overlayEl.getXY()[0]); switch (dragType) { case 'min': case 'max': style.cursor = 'ew-resize'; break; case 'pan': style.cursor = 'move'; break; default: style.cursor = 'default'; } }, getDragType: function(x) { var me = this, t = me.getTolerance(), width = me.overlaySurface.element.getSize().width, rangeMask = me.rangeMask, min = width * rangeMask.attr.min, max = width * rangeMask.attr.max, dragType; if (x > min + t && x < max - t) { dragType = 'pan'; } else if (x <= min + t && x > min - t) { dragType = 'min'; } else if (x >= max - t && x < max + t) { dragType = 'max'; } return dragType; }, onDragStart: function(e) { var me = this, x, dragType; // Limit drags to single touch. if (me.dragType || e && e.touches && e.touches.length > 1) { return; } x = e.touches[0].pageX - me.overlaySurface.element.getXY()[0]; dragType = me.getDragType(x); me.rangeMask.attr.thumbOpacity = 1; if (dragType) { me.dragType = dragType; me.touchId = e.touches[0].identifier; me.dragX = x; } }, onDrag: function(e) { if (e.touch.identifier !== this.touchId) { return; } // eslint-disable-next-line vars-on-top var me = this, overlayEl = me.overlaySurface.element, width = overlayEl.getSize().width, x = e.touches[0].pageX - overlayEl.getXY()[0], thumbGap = me.getThumbGap() / width, rangeMask = me.rangeMask, min = rangeMask.attr.min, max = rangeMask.attr.max, delta = max - min, dragType = me.dragType, drag = me.dragX, dx = (x - drag) / width; if (dragType === 'pan') { min += dx; max += dx; if (min < 0) { min = 0; max = delta; } if (max > 1) { max = 1; min = max - delta; } } else if (dragType === 'min') { min += dx; if (min < 0) { min = 0; } if (min > max - thumbGap) { min = max - thumbGap; } } else if (dragType === 'max') { max += dx; if (max > 1) { max = 1; } if (max < min + thumbGap) { max = min + thumbGap; } } else { return; } me.dragX = x; me.setVisibleRange([min, max]); }, onDragEnd: function() { var me = this, autoHideThumbs = me.getAutoHideThumbs(); me.dragType = null; if (autoHideThumbs) { me.rangeMask.setAttributes({ thumbOpacity: 0 }); } }, updateMinimum: function(mininum) { if (!this.isConfiguring) { this.setVisibleRange([mininum, this.getMaximum()]); } }, updateMaximum: function(maximum) { if (!this.isConfiguring) { this.setVisibleRange([this.getMinimum(), maximum]); } }, getMinimum: function() { return this.rangeMask.attr.min; }, getMaximum: function() { return this.rangeMask.attr.max; }, setVisibleRange: function(visibleRange) { var me = this, chart = me.chart; me.axis.setVisibleRange(visibleRange); me.rangeMask.setAttributes({ min: visibleRange[0], max: visibleRange[1] }); me.getSurface('overlay').renderFrame(); chart.suspendAnimation(); chart.redraw(); chart.resumeAnimation(); }, afterBoundChartLayout: function() { var me = this, spanSeries = me.getSpan() === 'series', mainRect = me.chart.getMainRect(), size = me.element.getSize(); if (mainRect && spanSeries) { me.setInsetPadding({ left: mainRect[0], right: size.width - mainRect[2] - mainRect[0], top: 0, bottom: 0 }); me.performLayout(); } }, afterChartLayout: function() { var me = this, size = me.overlaySurface.element.getSize(); me.rangeMask.setAttributes({ scalingCenterX: 0, scalingCenterY: 0, scalingX: size.width, scalingY: size.height }); }, doDestroy: function() { var chart = this.chart; if (chart && !chart.destroyed) { chart.un('layout', 'afterBoundChartLayout', this); } this.callParent(); }});