diff --git a/src/components/grid.ts b/src/components/grid.ts index c30e065..58b3f2d 100644 --- a/src/components/grid.ts +++ b/src/components/grid.ts @@ -2,49 +2,29 @@ import { GridOptions, SvgElParams } from '../types'; import * as d3 from 'd3'; -import defaultsDeep from 'lodash/defaultsDeep'; - -const DEFAULT_GRID_TICK_COUNT = 5; -const DEFAULT_GRID_OPTIONS: GridOptions = { - x: { - enabled: true, - ticksCount: DEFAULT_GRID_TICK_COUNT, - }, - y: { - enabled: true, - ticksCount: DEFAULT_GRID_TICK_COUNT, - }, -} - // Grid Class - is a core component, which can be a separate Pod in the future. (but not in current Pod terminology) // All components have construcor with required args: svg element which will be filled with this component and options for it. // All compoтents have a reqiured method "render", which will be called in core costructor. <- this solution is temporary. -// Each component has its own default options. // svgElement should be a separate class with its own height, width, xScale, yScale params to avoid SvgElParams as argument. // We have a general problem with passing d3 as argument everywhere. Fix it, and remove from arg in constructor here. export class Grid { - protected gridOptions: GridOptions; - constructor( private _svgEl: d3.Selection, private _svgElParams: SvgElParams, - _gridOptions: GridOptions, - ) { - this.gridOptions = this.setOptionDefaults(_gridOptions); - } - - protected setOptionDefaults(gridOptions: GridOptions): GridOptions { - return defaultsDeep(gridOptions, DEFAULT_GRID_OPTIONS); - } + protected gridOptions: GridOptions, + ) {} public render(): void { - // TODO: temporary. Move out of here - this._svgEl.selectAll('.grid').remove(); + this.clear(); this.renderGridLinesX(); this.renderGridLinesY(); - this.updateStylesOfTicks(); + } + + clear(): void { + // TODO: temporary. Move out of here + this._svgEl.selectAll('.grid').remove(); } renderGridLinesX(): void { @@ -79,12 +59,4 @@ export class Grid { .tickFormat(() => '') ); } - - updateStylesOfTicks(): void { - // TODO: add options for these actions - this._svgEl.selectAll('.grid').selectAll('.tick') - .attr('opacity', '0.5'); - this._svgEl.selectAll('.grid').select('.domain') - .style('pointer-events', 'none'); - } } diff --git a/src/css/style.css b/src/css/style.css index e203475..9b6a5b9 100755 --- a/src/css/style.css +++ b/src/css/style.css @@ -1,9 +1,14 @@ .grid path { stroke-width: 0; } - .grid line { stroke: lightgrey; stroke-opacity: 0.7; shape-rendering: crispEdges; } +.grid .tick { + opacity: 0.5; +} +.grid .domain { + pointer-events: none; +} diff --git a/src/index.ts b/src/index.ts index 256cc25..4cbfa32 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,5 @@ import VueChartwerkPodMixin from './VueChartwerkPodMixin'; -import { PodState } from './state'; +import { PodState } from './models/state'; import { Grid } from './components/grid'; import { CoreSeries } from './models/series'; @@ -165,8 +165,7 @@ abstract class ChartwerkPod { height: this.height, width: this.width, } - // TODO: use instanses instead of oblects - this.state = new PodState(boxPararms, this.coreSeries.visibleSeries, this.coreOptions.allOptions); + this.state = new PodState(boxPararms, this.coreSeries, this.coreOptions); } protected initComponents(): void { @@ -232,7 +231,7 @@ abstract class ChartwerkPod { .attr('id', 'x-axis-container') .style('pointer-events', 'none') .call( - d3.axisBottom(this.xScale) + d3.axisBottom(this.state.xScale) .ticks(this.coreOptions.axis.x.ticksCount) .tickSize(DEFAULT_TICK_SIZE) .tickFormat(this.getAxisTicksFormatter(this.coreOptions.axis.x)) @@ -251,7 +250,7 @@ abstract class ChartwerkPod { .style('pointer-events', 'none') // TODO: number of ticks shouldn't be hardcoded .call( - d3.axisLeft(this.yScale) + d3.axisLeft(this.state.yScale) .ticks(this.coreOptions.axis.y.ticksCount) .tickSize(DEFAULT_TICK_SIZE) .tickFormat(this.getAxisTicksFormatter(this.coreOptions.axis.y)) @@ -277,7 +276,7 @@ abstract class ChartwerkPod { .style('pointer-events', 'none') // TODO: number of ticks shouldn't be hardcoded .call( - d3.axisRight(this.y1Scale) + d3.axisRight(this.state.y1Scale) .ticks(DEFAULT_TICK_COUNT) .tickSize(DEFAULT_TICK_SIZE) .tickFormat(this.getAxisTicksFormatter(this.coreOptions.axis.y1)) @@ -416,10 +415,10 @@ abstract class ChartwerkPod { .attr('fill', 'none'); } - this.initScaleX = this.xScale.copy(); - this.initScaleY = this.yScale.copy(); + this.initScaleX = this.state.xScale.copy(); + this.initScaleY = this.state.yScale.copy(); if(this.coreOptions.axis.y1.isActive) { - this.initScaleY1 = this.y1Scale.copy(); + this.initScaleY1 = this.state.y1Scale.copy(); } this.pan = d3.zoom() .on('zoom', this.onPanning.bind(this)) @@ -611,18 +610,18 @@ abstract class ChartwerkPod { rescaleAxisX(transformX: number): void { this.state.transform = { x: transformX }; const rescaleX = d3.event.transform.rescaleX(this.initScaleX); - this.xAxisElement.call(d3.axisBottom(this.xScale).scale(rescaleX)); + this.xAxisElement.call(d3.axisBottom(this.state.xScale).scale(rescaleX)); this.state.xValueRange = [rescaleX.invert(0), rescaleX.invert(this.width)]; } rescaleAxisY(transformY: number): void { this.state.transform = { y: transformY }; const rescaleY = d3.event.transform.rescaleY(this.initScaleY); - this.yAxisElement.call(d3.axisLeft(this.yScale).scale(rescaleY)); + this.yAxisElement.call(d3.axisLeft(this.state.yScale).scale(rescaleY)); this.state.yValueRange = [rescaleY.invert(0), rescaleY.invert(this.height)]; if(this.y1AxisElement) { const rescaleY1 = d3.event.transform.rescaleY(this.initScaleY1); - this.y1AxisElement.call(d3.axisLeft(this.y1Scale).scale(rescaleY1)); + this.y1AxisElement.call(d3.axisLeft(this.state.y1Scale).scale(rescaleY1)); this.state.y1ValueRange = [rescaleY1.invert(0), rescaleY1.invert(this.height)]; // TODO: y1 axis jumps on panning this.y1AxisElement.selectAll('line').attr('x2', 2); @@ -639,7 +638,7 @@ abstract class ChartwerkPod { case ScrollPanOrientation.HORIZONTAL: // @ts-ignore const signX = Math.sign(event.transform.x); - const transformX = this.absXScale.invert(Math.abs(transformStep)); + const transformX = this.state.absXScale.invert(Math.abs(transformStep)); let rangeX = this.state.xValueRange; this.state.xValueRange = [rangeX[0] + signX * transformX, rangeX[1] + signX * transformX]; const translateX = this.state.transform.x + signX * transformStep; @@ -652,7 +651,7 @@ abstract class ChartwerkPod { signY = -signY; } let rangeY = this.state.yValueRange; - const transformY = this.absYScale.invert(transformStep); + const transformY = this.state.absYScale.invert(transformStep); this.deltaYTransform = this.deltaYTransform + transformStep; // TODO: not hardcoded bounds if(this.deltaYTransform > this.height * 0.9) { @@ -735,23 +734,23 @@ abstract class ChartwerkPod { let yRange: [number, number]; switch(this.coreOptions.mouseZoomEvent.orientation) { case BrushOrientation.HORIZONTAL: - const startTimestamp = this.xScale.invert(extent[0]); - const endTimestamp = this.xScale.invert(extent[1]); + const startTimestamp = this.state.xScale.invert(extent[0]); + const endTimestamp = this.state.xScale.invert(extent[1]); xRange = [startTimestamp, endTimestamp]; this.state.xValueRange = xRange; break; case BrushOrientation.VERTICAL: - const upperY = this.yScale.invert(extent[0]); - const bottomY = this.yScale.invert(extent[1]); + const upperY = this.state.yScale.invert(extent[0]); + const bottomY = this.state.yScale.invert(extent[1]); // TODO: add min zoom y yRange = [upperY, bottomY]; this.state.yValueRange = yRange; break; case BrushOrientation.RECTANGLE: - const bothStartTimestamp = this.xScale.invert(extent[0][0]); - const bothEndTimestamp = this.xScale.invert(extent[1][0]); - const bothUpperY = this.yScale.invert(extent[0][1]); - const bothBottomY = this.yScale.invert(extent[1][1]); + const bothStartTimestamp = this.state.xScale.invert(extent[0][0]); + const bothEndTimestamp = this.state.xScale.invert(extent[1][0]); + const bothUpperY = this.state.yScale.invert(extent[0][1]); + const bothBottomY = this.state.yScale.invert(extent[1][1]); xRange = [bothStartTimestamp, bothEndTimestamp]; yRange = [bothUpperY, bothBottomY]; this.state.xValueRange = xRange; @@ -762,10 +761,10 @@ abstract class ChartwerkPod { if(selectionAtts === undefined) { break; } - const scaledX0 = this.xScale.invert(selectionAtts.x); - const scaledX1 = this.xScale.invert(selectionAtts.x + selectionAtts.width); - const scaledY0 = this.yScale.invert(selectionAtts.y); - const scaledY1 = this.yScale.invert(selectionAtts.y + selectionAtts.height); + const scaledX0 = this.state.xScale.invert(selectionAtts.x); + const scaledX1 = this.state.xScale.invert(selectionAtts.x + selectionAtts.width); + const scaledY0 = this.state.yScale.invert(selectionAtts.y); + const scaledY1 = this.state.yScale.invert(selectionAtts.y + selectionAtts.height); xRange = [scaledX0, scaledX1]; yRange = [scaledY0, scaledY1]; this.state.xValueRange = xRange; @@ -777,8 +776,8 @@ abstract class ChartwerkPod { } protected zoomOut(): void { - let xAxisMiddleValue: number = this.xScale.invert(this.width / 2); - let yAxisMiddleValue: number = this.yScale.invert(this.height / 2); + let xAxisMiddleValue: number = this.state.xScale.invert(this.width / 2); + let yAxisMiddleValue: number = this.state.yScale.invert(this.height / 2); const centers = { x: xAxisMiddleValue, y: yAxisMiddleValue @@ -786,33 +785,6 @@ abstract class ChartwerkPod { this.coreOptions.callbackZoomOut(centers); } - // TODO: move to State - get absXScale(): d3.ScaleLinear { - const domain = [0, Math.abs(this.state.getMaxValueX() - this.state.getMinValueX())]; - return d3.scaleLinear() - .domain(domain) - .range([0, this.width]); - } - - get absYScale(): d3.ScaleLinear { - const domain = [0, Math.abs(this.state.getMaxValueY() - this.state.getMinValueY())]; - return d3.scaleLinear() - .domain(domain) - .range([0, this.height]); - } - - get xScale(): d3.ScaleLinear { - return this.state.xScale; - } - - get yScale(): d3.ScaleLinear { - return this.state.yScale; - } - - protected get y1Scale(): d3.ScaleLinear { - return this.state.y1Scale; - } - getd3TimeRangeEvery(count: number): d3.TimeInterval { if(this.coreOptions.allOptions.timeInterval.timeFormat === undefined) { return d3.timeMinute.every(count); diff --git a/src/models/series.ts b/src/models/series.ts index 86dde81..95a6b02 100644 --- a/src/models/series.ts +++ b/src/models/series.ts @@ -5,6 +5,10 @@ import lodashDefaultsDeep from 'lodash/defaultsDeep'; import lodashMap from 'lodash/map'; import lodashCloneDeep from 'lodash/cloneDeep'; import lodashUniq from 'lodash/uniq'; +import lodashMin from 'lodash/min'; +import lodashMinBy from 'lodash/minBy'; +import lodashMax from 'lodash/max'; +import lodashMaxBy from 'lodash/maxBy'; const SERIE_DEFAULTS = { @@ -70,4 +74,52 @@ export class CoreSeries { get rightYRelatedSeries(): Array { return this.visibleSeries.filter(serie => serie.yOrientation = yAxisOrientation.RIGHT); } + + get minValueY(): number { + return lodashMin( + this.leftYRelatedSeries.map( + serie => lodashMinBy(serie.datapoints, dp => dp[1])[1] + ) + ); + } + + get maxValueY(): number { + return lodashMax( + this.leftYRelatedSeries.map( + serie => lodashMaxBy(serie.datapoints, dp => dp[1])[1] + ) + ); + } + + get minValueX(): number { + return lodashMin( + this.visibleSeries.map( + serie => lodashMinBy(serie.datapoints, dp => dp[0])[0] + ) + ); + } + + get maxValueX(): number { + return lodashMax( + this.visibleSeries.map( + serie => lodashMaxBy(serie.datapoints, dp => dp[0])[0] + ) + ); + } + + get minValueY1(): number { + return lodashMin( + this.rightYRelatedSeries.map( + serie => lodashMinBy(serie.datapoints, dp => dp[1])[1] + ) + ); + } + + get maxValueY1(): number { + return lodashMax( + this.rightYRelatedSeries.map( + serie => lodashMaxBy(serie.datapoints, dp => dp[1])[1] + ) + ); + } } diff --git a/src/state.ts b/src/models/state.ts similarity index 55% rename from src/state.ts rename to src/models/state.ts index 75ee733..dab200f 100755 --- a/src/state.ts +++ b/src/models/state.ts @@ -1,4 +1,6 @@ -import { CoreSerie, Options, yAxisOrientation } from './types'; +import { CoreSerie, Options, yAxisOrientation } from '../types'; +import { CoreSeries } from './series'; +import { CoreOptions } from './options'; import * as d3 from 'd3'; @@ -20,7 +22,6 @@ const DEFAULT_TRANSFORM = { // 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. // TODO: PodState.transform has conflicts with d3.zoom.event.transform. It should be synchronized. export class PodState { private _xValueRange: [number, number]; @@ -33,8 +34,8 @@ export class PodState { constructor( protected boxParams: { height: number, width: number }, - protected series: T[], - protected options: O, + protected coreSeries: CoreSeries, + protected coreOptions: CoreOptions, ) { this.setInitialRanges(); this.initScales(); @@ -55,7 +56,7 @@ export class PodState { protected setYScale(): void { let domain = this._yValueRange; domain = sortBy(domain) as [number, number]; - if(this.options.axis.y.invert === true) { + if(this.coreOptions.axis.y.invert === true) { domain = reverse(domain); } this._yScale = d3.scaleLinear() @@ -64,7 +65,10 @@ export class PodState { } protected setXScale(): void { - const domain = this._xValueRange; + let domain = this._xValueRange; + if(this.coreOptions.axis.x.invert === true) { + domain = reverse(domain); + } this._xScale = d3.scaleLinear() .domain(domain) .range([0, this.boxParams.width]); @@ -73,7 +77,7 @@ export class PodState { protected setY1Scale(): void { let domain = this._y1ValueRange; domain = sortBy(domain) as [number, number]; - if(this.options.axis.y1.invert === true) { + if(this.coreOptions.axis.y1.invert === true) { domain = reverse(domain); } this._y1Scale = d3.scaleLinear() @@ -137,117 +141,79 @@ export class PodState { } public getMinValueY(): number { - if(this.isSeriesUnavailable) { + if(!this.coreSeries.isSeriesAvailable) { return DEFAULT_AXIS_RANGE[0]; } - if(this.options.axis.y !== undefined && this.options.axis.y.range !== undefined) { - return min(this.options.axis.y.range); + if(this.coreOptions.axis.y.range !== undefined) { + return min(this.coreOptions.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; + return this.coreSeries.minValueY; } public getMaxValueY(): number { - if(this.isSeriesUnavailable) { + if(!this.coreSeries.isSeriesAvailable) { return DEFAULT_AXIS_RANGE[1]; } - if(this.options.axis.y !== undefined && this.options.axis.y.range !== undefined) { - return max(this.options.axis.y.range); + if(this.coreOptions.axis.y.range !== undefined) { + return max(this.coreOptions.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; + return this.coreSeries.maxValueY; } public getMinValueX(): number { - if(this.isSeriesUnavailable) { + if(!this.coreSeries.isSeriesAvailable) { return DEFAULT_AXIS_RANGE[0]; } - if(this.options.axis.x !== undefined && this.options.axis.x.range !== undefined) { - return min(this.options.axis.x.range); + if(this.coreOptions.axis.x.range !== undefined) { + return min(this.coreOptions.axis.x.range); } - const minValue = min( - this.series - .filter(serie => serie.visible !== false) - .map( - serie => minBy(serie.datapoints, dp => dp[0])[0] - ) - ); - return minValue; + return this.coreSeries.minValueX; } public getMaxValueX(): number { - if(this.isSeriesUnavailable) { + if(!this.coreSeries.isSeriesAvailable) { return DEFAULT_AXIS_RANGE[1]; } - if(this.options.axis.x !== undefined && this.options.axis.x.range !== undefined) { - return max(this.options.axis.x.range) + + if(this.coreOptions.axis.x.range !== undefined) { + return max(this.coreOptions.axis.x.range); } - const maxValue = max( - this.series - .filter(serie => serie.visible !== false) - .map( - serie => maxBy(serie.datapoints, dp => dp[0])[0] - ) - ); - return maxValue; + return this.coreSeries.maxValueX; } public getMinValueY1(): number { - if(this.isSeriesUnavailable || this.options.axis.y1 === undefined || this.options.axis.y1.isActive === false) { + if(!this.coreSeries.isSeriesAvailable) { return DEFAULT_AXIS_RANGE[0]; } - if(this.options.axis.y1.range !== undefined) { - return min(this.options.axis.y1.range); + if(this.coreOptions.axis.y1.range !== undefined) { + return min(this.coreOptions.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; + return this.coreSeries.minValueY; } public getMaxValueY1(): number { - if(this.isSeriesUnavailable || this.options.axis.y1 === undefined || this.options.axis.y1.isActive === false) { + if(!this.coreSeries.isSeriesAvailable) { return DEFAULT_AXIS_RANGE[1]; } - if(this.options.axis.y1 !== undefined && this.options.axis.y1.range !== undefined) { - return max(this.options.axis.y1.range); + if(this.coreOptions.axis.y1.range !== undefined) { + return max(this.coreOptions.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; + return this.coreSeries.maxValueY; } - get isSeriesUnavailable(): boolean { - return this.series === undefined || this.series.length === 0 || - max(this.series.map(serie => serie.datapoints.length)) === 0; + // getters for correct transform + get absXScale(): d3.ScaleLinear { + const domain = [0, Math.abs(this.getMaxValueX() - this.getMinValueX())]; + return d3.scaleLinear() + .domain(domain) + .range([0, this.boxParams.width]); } - protected filterSerieByYAxisOrientation(serie: T, orientation: yAxisOrientation): boolean { - if(serie.yOrientation === undefined) { - return true; - } - return serie.yOrientation === orientation; + get absYScale(): d3.ScaleLinear { + const domain = [0, Math.abs(this.getMaxValueY() - this.getMinValueY())]; + return d3.scaleLinear() + .domain(domain) + .range([0, this.boxParams.height]); } }