diff --git a/src/index.ts b/src/index.ts index ac9ec2e..f53584c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,12 +1,13 @@ import VueChartwerkPodMixin from './VueChartwerkPodMixin'; import { PodState } from './state'; import { Grid } from './components/grid'; +import { CoreSeries } from 'models/series'; import styles from './css/style.css'; import { Margin, - TimeSerie, + CoreSerie, Options, TickOrientation, TimeFormat, @@ -109,7 +110,11 @@ const DEFAULT_OPTIONS: Options = { renderLegend: true, } -abstract class ChartwerkPod { +abstract class ChartwerkPod { + + protected coreSeries: CoreSeries; + protected options: O; + protected d3Node?: d3.Selection; protected customOverlay?: d3.Selection; protected crosshair?: d3.Selection; @@ -130,8 +135,7 @@ abstract class ChartwerkPod { protected y1AxisElement?: d3.Selection; protected yAxisTicksColors?: string[] = []; private _clipPathUID = ''; - protected series: T[]; - protected options: O; + protected readonly d3: typeof d3; protected deltaYTransform = 0; // TODO: forceRerender is a hack, it will be remove someday. But we need to update state on resize @@ -154,7 +158,8 @@ abstract class ChartwerkPod { let options = cloneDeep(_options); defaultsDeep(options, DEFAULT_OPTIONS); this.options = options; - this.series = cloneDeep(_series); + + this.coreSeries = new CoreSeries(_series); this.d3Node = d3.select(this.el); this.addEventListeners(); @@ -227,8 +232,7 @@ abstract class ChartwerkPod { if(newSeries === undefined) { return; } - let series = cloneDeep(newSeries); - this.series = series; + this.coreSeries.updateSeries(newSeries); } protected abstract renderMetrics(): void; @@ -243,7 +247,7 @@ abstract class ChartwerkPod { height: this.height, width: this.width, } - this.state = new PodState(boxPararms, this.series, this.options); + this.state = new PodState(boxPararms, this.coreSeries.visibleSeries, this.options); } protected initComponents(): void { @@ -521,46 +525,48 @@ abstract class ChartwerkPod { if(this.options.renderLegend === false) { return; } - if(this.series.length > 0) { - let legendRow = this.chartContainer - .append('g') - .attr('class', 'legend-row'); - for(let idx = 0; idx < this.series.length; idx++) { - if(includes(this.seriesTargetsWithBounds, this.series[idx].target)) { - continue; - } - let node = legendRow.selectAll('text').node(); - let rowWidth = 0; - if(node !== null) { - rowWidth = legendRow.node().getBBox().width + 25; - } - - const isChecked = this.series[idx].visible !== false; - legendRow.append('foreignObject') - .attr('x', rowWidth) - .attr('y', this.legendRowPositionY - 12) - .attr('width', 13) - .attr('height', 15) - .html(`
`) - .on('click', () => { - if(this.options.eventsCallbacks !== undefined && this.options.eventsCallbacks.onLegendClick !== undefined) { - this.options.eventsCallbacks.onLegendClick(idx); - } - }); - - legendRow.append('text') - .attr('x', rowWidth + 20) - .attr('y', this.legendRowPositionY) - .attr('class', `metric-legend-${idx}`) - .style('font-size', '12px') - .style('fill', this.getSerieColor(idx)) - .text(this.series[idx].target) - .on('click', () => { - if(this.options.eventsCallbacks !== undefined && this.options.eventsCallbacks.onLegendLabelClick !== undefined) { - this.options.eventsCallbacks.onLegendLabelClick(idx); - } - }); + if(!this.coreSeries.isSeriesAvailable) { + return; + } + let legendRow = this.chartContainer + .append('g') + .attr('class', 'legend-row'); + const series = this.coreSeries.allSeries; + for(let idx = 0; idx < series.length; idx++) { + if(includes(this.seriesTargetsWithBounds, series[idx].target)) { + continue; } + let node = legendRow.selectAll('text').node(); + let rowWidth = 0; + if(node !== null) { + rowWidth = legendRow.node().getBBox().width + 25; + } + + const isChecked = series[idx].visible !== false; + legendRow.append('foreignObject') + .attr('x', rowWidth) + .attr('y', this.legendRowPositionY - 12) + .attr('width', 13) + .attr('height', 15) + .html(`
`) + .on('click', () => { + if(this.options.eventsCallbacks !== undefined && this.options.eventsCallbacks.onLegendClick !== undefined) { + this.options.eventsCallbacks.onLegendClick(idx); + } + }); + + legendRow.append('text') + .attr('x', rowWidth + 20) + .attr('y', this.legendRowPositionY) + .attr('class', `metric-legend-${idx}`) + .style('font-size', '12px') + .style('fill', series[idx].color) + .text(series[idx].target) + .on('click', () => { + if(this.options.eventsCallbacks !== undefined && this.options.eventsCallbacks.onLegendLabelClick !== undefined) { + this.options.eventsCallbacks.onLegendLabelClick(idx); + } + }); } } @@ -585,7 +591,7 @@ abstract class ChartwerkPod { return; } let yPosition = this.height + this.margin.top + this.margin.bottom - 35; - if(this.series.length === 0) { + if(this.coreSeries.isSeriesAvailable) { yPosition += 20; } this.chartContainer.append('text') @@ -943,11 +949,11 @@ abstract class ChartwerkPod { } get serieTimestampRange(): number | undefined { - if(this.series.length === 0) { + if(!this.coreSeries.isSeriesAvailable) { return undefined; } - const startTimestamp = first(this.series[0].datapoints)[0]; - const endTimestamp = last(this.series[0].datapoints)[0]; + const startTimestamp = first(this.coreSeries.visibleSeries[0].datapoints)[0]; + const endTimestamp = last(this.coreSeries.visibleSeries[0].datapoints)[0]; return (endTimestamp - startTimestamp) / 1000; } @@ -982,8 +988,8 @@ abstract class ChartwerkPod { } get timeInterval(): number { - if(this.series !== undefined && this.series.length > 0 && this.series[0].datapoints.length > 1) { - const interval = this.series[0].datapoints[1][0] - this.series[0].datapoints[0][0]; + if(this.coreSeries.isSeriesAvailable && this.coreSeries.visibleSeries[0].datapoints.length > 1) { + const interval = this.coreSeries.visibleSeries[0].datapoints[1][0] - this.coreSeries.visibleSeries[0].datapoints[0][0]; return interval; } if(this.options.timeInterval !== undefined && this.options.timeInterval.count !== undefined) { @@ -1033,7 +1039,7 @@ abstract class ChartwerkPod { optionalMargin.left += 20; } } - if(this.series.length > 0) { + if(this.coreSeries.isSeriesAvailable) { optionalMargin.bottom += 25; } return optionalMargin; @@ -1067,19 +1073,6 @@ abstract class ChartwerkPod { this.state.clearState(); } - protected getSerieColor(idx: number): string { - if(this.series[idx] === undefined) { - throw new Error( - `Can't get color for unexisting serie: ${idx}, there are only ${this.series.length} series` - ); - } - let serieColor = this.series[idx].color; - if(serieColor === undefined) { - serieColor = palette[idx % palette.length]; - } - return serieColor; - } - protected get seriesTargetsWithBounds(): any[] { if( this.options.bounds === undefined || @@ -1089,17 +1082,13 @@ abstract class ChartwerkPod { return []; } let series = []; - this.series.forEach(serie => { + this.coreSeries.allSeries.forEach(serie => { series.push(this.formattedBound(this.options.bounds.upper, serie.target)); series.push(this.formattedBound(this.options.bounds.lower, serie.target)); }); return series; } - protected get visibleSeries(): any[] { - return this.series.filter(serie => serie.visible !== false); - } - protected get rectClipId(): string { if(this._clipPathUID.length === 0) { this._clipPathUID = uid(); @@ -1123,7 +1112,7 @@ abstract class ChartwerkPod { export { ChartwerkPod, VueChartwerkPodMixin, - Margin, TimeSerie, Options, TickOrientation, TimeFormat, BrushOrientation, PanOrientation, + Margin, CoreSerie, Options, TickOrientation, TimeFormat, BrushOrientation, PanOrientation, AxisFormat, yAxisOrientation, CrosshairOrientation, ScrollPanOrientation, ScrollPanDirection, KeyEvent, palette }; diff --git a/src/models/options.ts b/src/models/options.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/models/series.ts b/src/models/series.ts new file mode 100644 index 0000000..9c2c37c --- /dev/null +++ b/src/models/series.ts @@ -0,0 +1,71 @@ +import { CoreSerie, yAxisOrientation } from '../types'; +import { palette } from '../colors'; + +import lodashDefaultsDeep from 'lodash/defaultsDeep'; +import lodashMap from 'lodash/map'; +import lodashCloneDeep from 'lodash/cloneDeep'; +import lodashUniq from 'lodash/uniq'; + + +const SERIE_DEFAULTS = { + alias: '', + visible: true, + yOrientation: yAxisOrientation.LEFT, + datapoints: [], + // fields below will be set in "fillDefaults" method + idx: undefined, + color: undefined, +}; + +export class CoreSeries { + _series: Array = []; + + constructor(series: T[]) { + this.setSeries(series); + } + + public updateSeries(series: T[]): void { + this.setSeries(series); + } + + protected setSeries(series: T[]): void { + this._series = lodashMap(series, (serie, serieIdx) => this.fillDefaults(lodashCloneDeep(serie), serieIdx)); + this.ensureSeriesValid(this._series); + } + + ensureSeriesValid(series: T[]): void { + const targets = lodashMap(series, serie => serie.target); + const uniqTargets = lodashUniq(targets); + if(uniqTargets.length !== series.length) { + throw new Error(`All serie.target should be uniq`); + } + } + + protected fillDefaults(serie: T, idx: number): T { + let defaults = lodashCloneDeep(SERIE_DEFAULTS); + defaults.color = palette[idx % palette.length]; + defaults.idx = idx; + lodashDefaultsDeep(serie, defaults); + return serie; + } + + get isSeriesAvailable(): boolean { + return this.visibleSeries.length > 0; + } + + get visibleSeries(): Array { + return this._series.filter(serie => serie.visible); + } + + get allSeries(): Array { + return this._series; + } + + get leftYRelatedSeries(): Array { + return this.visibleSeries.filter(serie => serie.yOrientation = yAxisOrientation.LEFT); + } + + get rightYRelatedSeries(): Array { + return this.visibleSeries.filter(serie => serie.yOrientation = yAxisOrientation.RIGHT); + } +} diff --git a/src/state.ts b/src/state.ts index aec86b8..6d82239 100755 --- a/src/state.ts +++ b/src/state.ts @@ -1,4 +1,4 @@ -import { TimeSerie, Options, yAxisOrientation } from './types'; +import { CoreSerie, Options, yAxisOrientation } from './types'; import * as d3 from 'd3'; @@ -22,7 +22,7 @@ const DEFAULT_TRANSFORM = { // 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 { +export class PodState { private _xValueRange: [number, number]; private _yValueRange: [number, number]; private _y1ValueRange: [number, number]; diff --git a/src/types.ts b/src/types.ts index 6e55391..f16fa25 100644 --- a/src/types.ts +++ b/src/types.ts @@ -4,9 +4,10 @@ export type Margin = { top: number, right: number, bottom: number, left: number export type Timestamp = number; // TODO: Pods can render not only "time" series -export type TimeSerie = { +export type CoreSerie = { target: string, datapoints: [Timestamp, number][], + idx?: number, alias?: string, visible?: boolean, color?: string,