Browse Source

series model

merge-requests/18/head
vargburz 2 years ago
parent
commit
073993d663
  1. 135
      src/index.ts
  2. 0
      src/models/options.ts
  3. 71
      src/models/series.ts
  4. 4
      src/state.ts
  5. 3
      src/types.ts

135
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<T extends TimeSerie, O extends Options> {
abstract class ChartwerkPod<T extends CoreSerie, O extends Options> {
protected coreSeries: CoreSeries<T>;
protected options: O;
protected d3Node?: d3.Selection<HTMLElement, unknown, null, undefined>;
protected customOverlay?: d3.Selection<SVGRectElement, unknown, null, undefined>;
protected crosshair?: d3.Selection<SVGGElement, unknown, null, undefined>;
@ -130,8 +135,7 @@ abstract class ChartwerkPod<T extends TimeSerie, O extends Options> {
protected y1AxisElement?: d3.Selection<SVGGElement, unknown, null, undefined>;
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<T extends TimeSerie, O extends Options> {
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<T extends TimeSerie, O extends Options> {
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<T extends TimeSerie, O extends Options> {
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<T extends TimeSerie, O extends Options> {
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(`<form><input type=checkbox ${isChecked? 'checked' : ''} /></form>`)
.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(`<form><input type=checkbox ${isChecked? 'checked' : ''} /></form>`)
.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<T extends TimeSerie, O extends Options> {
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<T extends TimeSerie, O extends Options> {
}
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<T extends TimeSerie, O extends Options> {
}
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<T extends TimeSerie, O extends Options> {
optionalMargin.left += 20;
}
}
if(this.series.length > 0) {
if(this.coreSeries.isSeriesAvailable) {
optionalMargin.bottom += 25;
}
return optionalMargin;
@ -1067,19 +1073,6 @@ abstract class ChartwerkPod<T extends TimeSerie, O extends Options> {
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<T extends TimeSerie, O extends Options> {
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<T extends TimeSerie, O extends Options> {
export {
ChartwerkPod, VueChartwerkPodMixin,
Margin, TimeSerie, Options, TickOrientation, TimeFormat, BrushOrientation, PanOrientation,
Margin, CoreSerie, Options, TickOrientation, TimeFormat, BrushOrientation, PanOrientation,
AxisFormat, yAxisOrientation, CrosshairOrientation, ScrollPanOrientation, ScrollPanDirection, KeyEvent,
palette
};

0
src/models/options.ts

71
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<T extends CoreSerie> {
_series: Array<T> = [];
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<T> {
return this._series.filter(serie => serie.visible);
}
get allSeries(): Array<T> {
return this._series;
}
get leftYRelatedSeries(): Array<T> {
return this.visibleSeries.filter(serie => serie.yOrientation = yAxisOrientation.LEFT);
}
get rightYRelatedSeries(): Array<T> {
return this.visibleSeries.filter(serie => serie.yOrientation = yAxisOrientation.RIGHT);
}
}

4
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<T extends TimeSerie, O extends Options> {
export class PodState<T extends CoreSerie, O extends Options> {
private _xValueRange: [number, number];
private _yValueRange: [number, number];
private _y1ValueRange: [number, number];

3
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,

Loading…
Cancel
Save