Browse Source

Merge branch 'oop-core-part-2' into 'main'

OOP core part 2

See merge request chartwerk/core!19
merge-requests/20/merge
rozetko 3 years ago
parent
commit
4b3775c571
  1. 44
      src/components/grid.ts
  2. 7
      src/css/style.css
  3. 82
      src/index.ts
  4. 52
      src/models/series.ts
  5. 128
      src/models/state.ts

44
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<SVGElement, unknown, null, undefined>,
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');
}
}

7
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;
}

82
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<T extends CoreSerie, O extends Options> {
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<T extends CoreSerie, O extends Options> {
.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<T extends CoreSerie, O extends Options> {
.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<T extends CoreSerie, O extends Options> {
.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<T extends CoreSerie, O extends Options> {
.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<T extends CoreSerie, O extends Options> {
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<T extends CoreSerie, O extends Options> {
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<T extends CoreSerie, O extends Options> {
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<T extends CoreSerie, O extends Options> {
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<T extends CoreSerie, O extends Options> {
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<T extends CoreSerie, O extends Options> {
}
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<T extends CoreSerie, O extends Options> {
this.coreOptions.callbackZoomOut(centers);
}
// TODO: move to State
get absXScale(): d3.ScaleLinear<number, number> {
const domain = [0, Math.abs(this.state.getMaxValueX() - this.state.getMinValueX())];
return d3.scaleLinear()
.domain(domain)
.range([0, this.width]);
}
get absYScale(): d3.ScaleLinear<number, number> {
const domain = [0, Math.abs(this.state.getMaxValueY() - this.state.getMinValueY())];
return d3.scaleLinear()
.domain(domain)
.range([0, this.height]);
}
get xScale(): d3.ScaleLinear<number, number> {
return this.state.xScale;
}
get yScale(): d3.ScaleLinear<number, number> {
return this.state.yScale;
}
protected get y1Scale(): d3.ScaleLinear<number, number> {
return this.state.y1Scale;
}
getd3TimeRangeEvery(count: number): d3.TimeInterval {
if(this.coreOptions.allOptions.timeInterval.timeFormat === undefined) {
return d3.timeMinute.every(count);

52
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<T extends CoreSerie> {
get rightYRelatedSeries(): Array<T> {
return this.visibleSeries.filter(serie => serie.yOrientation = yAxisOrientation.RIGHT);
}
get minValueY(): number {
return lodashMin(
this.leftYRelatedSeries.map(
serie => lodashMinBy<number[]>(serie.datapoints, dp => dp[1])[1]
)
);
}
get maxValueY(): number {
return lodashMax(
this.leftYRelatedSeries.map(
serie => lodashMaxBy<number[]>(serie.datapoints, dp => dp[1])[1]
)
);
}
get minValueX(): number {
return lodashMin(
this.visibleSeries.map(
serie => lodashMinBy<number[]>(serie.datapoints, dp => dp[0])[0]
)
);
}
get maxValueX(): number {
return lodashMax(
this.visibleSeries.map(
serie => lodashMaxBy<number[]>(serie.datapoints, dp => dp[0])[0]
)
);
}
get minValueY1(): number {
return lodashMin(
this.rightYRelatedSeries.map(
serie => lodashMinBy<number[]>(serie.datapoints, dp => dp[1])[1]
)
);
}
get maxValueY1(): number {
return lodashMax(
this.rightYRelatedSeries.map(
serie => lodashMaxBy<number[]>(serie.datapoints, dp => dp[1])[1]
)
);
}
}

128
src/state.ts → 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<T extends CoreSerie, O extends Options> {
private _xValueRange: [number, number];
@ -33,8 +34,8 @@ export class PodState<T extends CoreSerie, O extends Options> {
constructor(
protected boxParams: { height: number, width: number },
protected series: T[],
protected options: O,
protected coreSeries: CoreSeries<T>,
protected coreOptions: CoreOptions<O>,
) {
this.setInitialRanges();
this.initScales();
@ -55,7 +56,7 @@ export class PodState<T extends CoreSerie, O extends Options> {
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<T extends CoreSerie, O extends Options> {
}
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<T extends CoreSerie, O extends Options> {
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<T extends CoreSerie, O extends Options> {
}
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<number[]>(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<number[]>(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<number[]>(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<number[]>(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<number[]>(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<number[]>(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<number, number> {
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<number, number> {
const domain = [0, Math.abs(this.getMaxValueY() - this.getMinValueY())];
return d3.scaleLinear()
.domain(domain)
.range([0, this.boxParams.height]);
}
}
Loading…
Cancel
Save