UNPKG

veritatisquam

Version:
282 lines (246 loc) 8.26 kB
// Internal Imports import { AxisPositions, Events, ScaleTypes } from '../interfaces'; import { Service } from './service'; import { Tools } from '../tools'; import * as Configuration from '../configuration'; import { ChartModelCartesian } from '../model-cartesian-charts'; // D3 imports import { extent } from 'd3-array'; export class Zoom extends Service { protected model: ChartModelCartesian; isZoomBarEnabled() { // CartesianScales service is only available in axis charts if (!this.services.cartesianScales) { return false; } // @todo - need to update this if zoom bar in other position (bottom, left, right) is supported // check configuration if ( !Tools.getProperty( this.model.getOptions(), 'zoomBar', 'top', 'enabled' ) ) { return false; } // @todo - Zoom Bar only supports main axis at BOTTOM axis and time scale for now this.services.cartesianScales.findDomainAndRangeAxes(); // need to do this before getMainXAxisPosition() const mainXAxisPosition = this.services.cartesianScales.getMainXAxisPosition(); const mainXScaleType = Tools.getProperty( this.model.getOptions(), 'axes', mainXAxisPosition, 'scaleType' ); return ( mainXAxisPosition === AxisPositions.BOTTOM && mainXScaleType === ScaleTypes.TIME ); } // get display data for zoom bar // basically it's sum of value grouped by time getZoomBarData() { const customZoomBarData = this.model.getZoomBarData(); // if user already defines zoom bar data, use it if (customZoomBarData && customZoomBarData.length > 1) { return customZoomBarData; } else { // use displayData if not defined return this.model.getDisplayData(); } } getDefaultZoomBarDomain(zoomBarData?) { const allZoomBarData = zoomBarData || this.services.zoom.getZoomBarData(); const { cartesianScales } = this.services; const mainXAxisPosition = cartesianScales.getMainXAxisPosition(); const domainIdentifier = cartesianScales.getDomainIdentifier(); // default to full range with extended domain return cartesianScales.extendsDomain( mainXAxisPosition, extent(allZoomBarData, (d: any) => d[domainIdentifier]) ); } handleDomainChange(newDomain, configs = { dispatchEvent: true }) { this.model.set({ zoomDomain: newDomain }, { animate: false }); if (configs.dispatchEvent) { this.services.events.dispatchEvent(Events.ZoomDomain.CHANGE, { newDomain, }); } } getZoomRatio() { return Tools.getProperty( this.model.getOptions(), 'zoomBar', 'zoomRatio' ); } // filter out data not inside zoom domain // to get better range value for axis label filterDataForRangeAxis(displayData: object[], configs?: any) { const zoomDomain = this.model.get('zoomDomain'); const mergedConfigs = Object.assign( { stacked: false }, // default configs configs ); const shouldUpdateRangeAxis = Tools.getProperty( this.model.getOptions(), 'zoomBar', 'updateRangeAxis' ); if (this.isZoomBarEnabled() && shouldUpdateRangeAxis && zoomDomain) { const domainIdentifier = mergedConfigs.stacked ? 'sharedStackKey' : this.services.cartesianScales.getDomainIdentifier(); const filteredData = displayData.filter( (datum) => new Date(datum[domainIdentifier]) >= zoomDomain[0] && new Date(datum[domainIdentifier]) <= zoomDomain[1] ); // if no data in zoom domain, use all data to get full range value // so only return filteredData if length > 0 if (filteredData.length > 0) { return filteredData; } } // return original data by default return displayData; } zoomIn(zoomRatio = this.getZoomRatio()) { // get current zoomDomain const currentZoomDomain = this.model.get('zoomDomain'); const handleWidth = Configuration.zoomBar.handleWidth; const xScale = this.services.cartesianScales.getMainXScale().copy(); xScale.domain(this.getDefaultZoomBarDomain()); // reset domain to default full domain // use scale range (rather than domain) to calculate // current zoom bar handle x position const currentX0 = xScale(currentZoomDomain[0]); const currentX1 = xScale(currentZoomDomain[1]); // zoom bar handles are already too close if (currentX1 - currentX0 < handleWidth + 1) { return; } const fullRange = xScale.range(); const gap = currentX1 - currentX0; const diff = Math.min( ((fullRange[1] - fullRange[0]) / 2) * (zoomRatio / 2), gap / 2 ); // new zoom bar handle x position let newX0 = currentX0 + diff; let newX1 = currentX1 - diff; // if left handle becomes right side of right handle, just make them close to each other if (newX0 >= newX1) { newX0 = currentX0 + gap / 2 - handleWidth / 2; newX1 = currentX1 - gap / 2 + handleWidth / 2; } const newDomain = [xScale.invert(newX0), xScale.invert(newX1)]; // only if zoomDomain needs update if ( currentZoomDomain[0].valueOf() !== newDomain[0].valueOf() || currentZoomDomain[1].valueOf() !== newDomain[1].valueOf() ) { this.handleDomainChange(newDomain); } } zoomOut(zoomRatio = this.getZoomRatio()) { // get current zoomDomain const currentZoomDomain = this.model.get('zoomDomain'); const xScale = this.services.cartesianScales.getMainXScale().copy(); xScale.domain(this.getDefaultZoomBarDomain()); // reset domain to default full domain // use scale range (rather than domain) to calculate // current zoom bar handle x position const currentX0 = xScale(currentZoomDomain[0]); const currentX1 = xScale(currentZoomDomain[1]); const fullRange = xScale.range(); const diff = ((fullRange[1] - fullRange[0]) / 2) * (zoomRatio / 2); // new zoom bar handle x position // max to full range const newX0 = Math.max(currentX0 - diff, fullRange[0]); const newX1 = Math.min(currentX1 + diff, fullRange[1]); const newDomain = [xScale.invert(newX0), xScale.invert(newX1)]; // only if zoomDomain needs update if ( currentZoomDomain[0].valueOf() !== newDomain[0].valueOf() || currentZoomDomain[1].valueOf() !== newDomain[1].valueOf() ) { this.handleDomainChange(newDomain); } } resetZoomDomain() { // get current zoomDomain const currentZoomDomain = this.model.get('zoomDomain'); const newDomain = this.getDefaultZoomBarDomain(); // only if zoomDomain needs update if ( currentZoomDomain[0].valueOf() !== newDomain[0].valueOf() || currentZoomDomain[1].valueOf() !== newDomain[1].valueOf() ) { this.handleDomainChange(newDomain); } } // check if current zoom domain is already the min zoom domain // when toolbar is rendered, we don't render chart yet // don't depend on scale range isMinZoomDomain() { // get current zoomDomain const currentZoomDomain = this.model.get('zoomDomain'); // assume the max zoom domain is the default zoom bar domain const maxZoomDomain = this.getDefaultZoomBarDomain(); if (!currentZoomDomain || !maxZoomDomain) { return false; } const currentZoomDomainPeriod = currentZoomDomain[1].valueOf() - currentZoomDomain[0].valueOf(); const maxZoomDomainPeriod = maxZoomDomain[1].valueOf() - maxZoomDomain[0].valueOf(); const minZoomRatio = Tools.getProperty( this.model.getOptions(), 'zoomBar', 'minZoomRatio' ); // if current zoom domain is already smaller than minZoomRatio if (currentZoomDomainPeriod / maxZoomDomainPeriod < minZoomRatio) { return true; } return false; } // check if current zoom domain is already the max zoom domain isMaxZoomDomain() { // get current zoom domain const currentZoomDomain = this.model.get('zoomDomain'); // assume the max zoom domain is the default zoom bar domain const maxZoomDomain = this.getDefaultZoomBarDomain(); if ( currentZoomDomain && maxZoomDomain && currentZoomDomain[0].valueOf() === maxZoomDomain[0].valueOf() && currentZoomDomain[1].valueOf() === maxZoomDomain[1].valueOf() ) { return true; } return false; } isEmptyState() { return this.getZoomBarData().length === 0; } isZoomBarLoading(position) { return Tools.getProperty( this.model.getOptions(), 'zoomBar', position, 'loading' ); } isZoomBarLocked(position) { return Tools.getProperty( this.model.getOptions(), 'zoomBar', position, 'locked' ); } }