import { TimeSerie, Options, yAxisOrientation } from './types'; // we import only d3 types here import * as d3 from 'd3'; import cloneDeep from 'lodash/cloneDeep'; import min from 'lodash/min'; import minBy from 'lodash/minBy'; import max from 'lodash/max'; import maxBy from 'lodash/maxBy'; import sortBy from 'lodash/sortBy'; import reverse from 'lodash/reverse'; const DEFAULT_AXIS_RANGE = [0, 1]; const DEFAULT_TRANSFORM = { x: 0, y: 0, k: 1 } // TODO: replace all getters with fields. Because getters will be recalculated on each call. Use scales as example. // TODO: remove duplicates in max/min values. // TODO: PodState can be divided in two classes, but it is hard now. export class PodState { private _xValueRange: [number, number]; private _yValueRange: [number, number]; private _y1ValueRange: [number, number]; private _transform: { x: number, y: number, k: number } = cloneDeep(DEFAULT_TRANSFORM); private _xScale: d3.ScaleLinear; private _yScale: d3.ScaleLinear; private _y1Scale: d3.ScaleLinear; constructor( protected _d3: typeof d3, protected boxParams: { height: number, width: number }, protected series: T[], protected options: O, ) { this.setInitialRanges(); this.initScales(); } protected setInitialRanges(): void { this._xValueRange = [this.minValueX, this.maxValueX]; this._yValueRange = [this.minValueY, this.maxValueY]; this._y1ValueRange = [this.minValueY1, this.maxValueY1]; } protected initScales(): void { this.setXScale(); this.setYScale(); this.setY1Scale(); } protected setYScale(): void { let domain = this._yValueRange; domain = sortBy(domain) as [number, number]; if(this.options.axis.y.invert === true) { domain = reverse(domain); } this._yScale = this._d3.scaleLinear() .domain(domain) .range([this.boxParams.height, 0]); // inversed, because d3 y-axis goes from top to bottom; } protected setXScale(): void { const domain = this._xValueRange; this._xScale = this._d3.scaleLinear() .domain(domain) .range([0, this.boxParams.width]); } protected setY1Scale(): void { let domain = this._y1ValueRange; domain = sortBy(domain) as [number, number]; if(this.options.axis.y1.invert === true) { domain = reverse(domain); } this._y1Scale = this._d3.scaleLinear() .domain(domain) .range([this.boxParams.height, 0]); // inversed, because d3 y-axis goes from top to bottom } public clearState(): void { this.setInitialRanges(); this.initScales(); this._transform = { x: 0, y: 0, k: 1 }; } get yScale(): d3.ScaleLinear { return this._yScale; } get xScale(): d3.ScaleLinear { return this._xScale; } get y1Scale(): d3.ScaleLinear { return this._y1Scale; } get xValueRange(): [number, number] | undefined { return this._xValueRange; } get yValueRange(): [number, number] | undefined { return this._yValueRange; } get y1ValueRange(): [number, number] | undefined { return this._y1ValueRange; } get transform(): { x?: number, y?: number, k?: number } { return this._transform; } set xValueRange(range: [number, number]) { this._xValueRange = range; this.setXScale(); } set yValueRange(range: [number, number]) { this._yValueRange = range; this.setYScale(); } set y1ValueRange(range: [number, number]) { this._y1ValueRange = range; this.setY1Scale(); } set transform(transform: { x?: number, y?: number, k?: number }) { this._transform.x = transform.x !== undefined ? transform.x : this._transform.x; this._transform.y = transform.y !== undefined ? transform.y : this._transform.y; this._transform.k = transform.k !== undefined ? transform.k : this._transform.k; } get minValueY(): number { if(this.isSeriesUnavailable) { return DEFAULT_AXIS_RANGE[0]; } if(this.options.axis.y !== undefined && this.options.axis.y.range !== undefined) { return min(this.options.axis.y.range); } const minValue = min( this.series .filter(serie => serie.visible !== false && this.filterSerieByYAxisOrientation(serie, yAxisOrientation.LEFT)) .map( serie => minBy(serie.datapoints, dp => dp[1])[1] ) ); return minValue; } get maxValueY(): number { if(this.isSeriesUnavailable) { return DEFAULT_AXIS_RANGE[1]; } if(this.options.axis.y !== undefined && this.options.axis.y.range !== undefined) { return max(this.options.axis.y.range); } const maxValue = max( this.series .filter(serie => serie.visible !== false && this.filterSerieByYAxisOrientation(serie, yAxisOrientation.LEFT)) .map( serie => maxBy(serie.datapoints, dp => dp[1])[1] ) ); return maxValue; } get minValueX(): number { if(this.isSeriesUnavailable) { return DEFAULT_AXIS_RANGE[0]; } if(this.options.axis.x !== undefined && this.options.axis.x.range !== undefined) { return min(this.options.axis.x.range); } const minValue = min( this.series .filter(serie => serie.visible !== false) .map( serie => minBy(serie.datapoints, dp => dp[0])[0] ) ); return minValue; } get maxValueX(): number { if(this.isSeriesUnavailable) { return DEFAULT_AXIS_RANGE[1]; } if(this.options.axis.x !== undefined && this.options.axis.x.range !== undefined) { return max(this.options.axis.x.range) } const maxValue = max( this.series .filter(serie => serie.visible !== false) .map( serie => maxBy(serie.datapoints, dp => dp[0])[0] ) ); return maxValue; } get minValueY1(): number { if(this.isSeriesUnavailable || this.options.axis.y1 === undefined || this.options.axis.y1.isActive === false) { return DEFAULT_AXIS_RANGE[0]; } if(this.options.axis.y1.range !== undefined) { return min(this.options.axis.y1.range); } const minValue = min( this.series .filter(serie => serie.visible !== false && this.filterSerieByYAxisOrientation(serie, yAxisOrientation.RIGHT)) .map( serie => minBy(serie.datapoints, dp => dp[1])[1] ) ); return minValue; } get maxValueY1(): number { if(this.isSeriesUnavailable || this.options.axis.y1 === undefined || this.options.axis.y1.isActive === false) { return DEFAULT_AXIS_RANGE[1]; } if(this.options.axis.y1 !== undefined && this.options.axis.y1.range !== undefined) { return max(this.options.axis.y1.range); } const maxValue = max( this.series .filter(serie => serie.visible !== false && this.filterSerieByYAxisOrientation(serie, yAxisOrientation.RIGHT)) .map( serie => maxBy(serie.datapoints, dp => dp[1])[1] ) ); return maxValue; } get isSeriesUnavailable(): boolean { return this.series === undefined || this.series.length === 0 || max(this.series.map(serie => serie.datapoints.length)) === 0; } protected filterSerieByYAxisOrientation(serie: T, orientation: yAxisOrientation): boolean { if(serie.yOrientation === undefined || serie.yOrientation === yAxisOrientation.BOTH) { return true; } return serie.yOrientation === orientation; } }