diff --git a/src/index.ts b/src/index.ts index f53584c..9f47c38 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,9 @@ import VueChartwerkPodMixin from './VueChartwerkPodMixin'; import { PodState } from './state'; import { Grid } from './components/grid'; + import { CoreSeries } from 'models/series'; +import { CoreOptions } from 'models/options'; import styles from './css/style.css'; @@ -9,7 +11,6 @@ import { Margin, CoreSerie, Options, - TickOrientation, TimeFormat, BrushOrientation, AxisFormat, @@ -21,6 +22,7 @@ import { ScrollPanOrientation, ScrollPanDirection, AxisOption, + AxesOptions, } from './types'; import { uid } from './utils'; import { palette } from './colors'; @@ -43,77 +45,11 @@ const DEFAULT_MARGIN: Margin = { top: 30, right: 20, bottom: 20, left: 30 }; const DEFAULT_TICK_COUNT = 4; const DEFAULT_TICK_SIZE = 2; const MILISECONDS_IN_MINUTE = 60 * 1000; -const DEFAULT_SCROLL_PAN_STEP = 50; -const DEFAULT_OPTIONS: Options = { - confidence: 0, - timeInterval: { - timeFormat: TimeFormat.MINUTE - }, - tickFormat: { - xAxis: '%H:%M', - xTickOrientation: TickOrientation.HORIZONTAL - }, - zoomEvents: { - mouse: { - zoom: { - isActive: true, - keyEvent: KeyEvent.MAIN, - orientation: BrushOrientation.HORIZONTAL - }, - pan: { - isActive: true, - keyEvent: KeyEvent.SHIFT, - orientation: PanOrientation.HORIZONTAL - }, - doubleClick: { - isActive: true, - keyEvent: KeyEvent.MAIN, - }, - }, - scroll: { - zoom: { - isActive: true, - keyEvent: KeyEvent.MAIN, - orientation: PanOrientation.BOTH, - }, - pan: { - isActive: false, - keyEvent: KeyEvent.SHIFT, - panStep: DEFAULT_SCROLL_PAN_STEP, - orientation: ScrollPanOrientation.HORIZONTAL, - direction: ScrollPanDirection.BOTH, - }, - }, - }, - axis: { - x: { - isActive: true, - ticksCount: DEFAULT_TICK_COUNT, - format: AxisFormat.TIME - }, - y: { - isActive: true, - ticksCount: DEFAULT_TICK_COUNT, - format: AxisFormat.NUMERIC - }, - y1: { - isActive: false, - ticksCount: DEFAULT_TICK_COUNT, - format: AxisFormat.NUMERIC - } - }, - crosshair: { - orientation: CrosshairOrientation.VERTICAL, - color: 'red' - }, - renderTicksfromTimestamps: false, - renderLegend: true, -} abstract class ChartwerkPod { protected coreSeries: CoreSeries; - protected options: O; + protected coreOptions: CoreOptions; protected d3Node?: d3.Selection; protected customOverlay?: d3.Selection; @@ -155,10 +91,7 @@ abstract class ChartwerkPod { // TODO: test if it's necessary styles.use(); - let options = cloneDeep(_options); - defaultsDeep(options, DEFAULT_OPTIONS); - this.options = options; - + this.coreOptions = new CoreOptions(_options); this.coreSeries = new CoreSeries(_series); this.d3Node = d3.select(this.el); @@ -178,9 +111,7 @@ abstract class ChartwerkPod { } public render(): void { - if(has(this.options.eventsCallbacks, 'renderStart')) { - this.options.eventsCallbacks.renderStart(); - } + this.coreOptions.callbackRenderStart(); this.renderClipPath(); this.addEvents(); @@ -196,9 +127,7 @@ abstract class ChartwerkPod { this.renderYLabel(); this.renderXLabel(); - if(has(this.options.eventsCallbacks, 'renderEnd')) { - this.options.eventsCallbacks.renderEnd(); - } + this.coreOptions.callbackRenderEnd(); } public updateData(series?: T[], options?: O, shouldRerender = true): void { @@ -222,10 +151,7 @@ abstract class ChartwerkPod { if(newOptions === undefined) { return; } - let options = cloneDeep(newOptions); - defaultsDeep(options, DEFAULT_OPTIONS); - this.options = options; - // TODO: update state if axis ranges were changed + this.coreOptions.updateOptions(newOptions); } protected updateSeries(newSeries: T[]): void { @@ -247,7 +173,7 @@ abstract class ChartwerkPod { height: this.height, width: this.width, } - this.state = new PodState(boxPararms, this.coreSeries.visibleSeries, this.options); + this.state = new PodState(boxPararms, this.coreSeries.visibleSeries, this.coreOptions.allOptions); } protected initComponents(): void { @@ -260,7 +186,7 @@ abstract class ChartwerkPod { yScale: this.state.yScale, } - this.grid = new Grid(this.chartContainer, svgElParams, this.options.grid); + this.grid = new Grid(this.chartContainer, svgElParams, this.coreOptions.grid); } protected renderMetricsContainer(): void { @@ -303,7 +229,7 @@ abstract class ChartwerkPod { } protected renderXAxis(): void { - if(this.options.axis.x.isActive === false) { + if(this.coreOptions.axis.x.isActive === false) { return; } this.chartContainer.select('#x-axis-container').remove(); @@ -314,16 +240,14 @@ abstract class ChartwerkPod { .style('pointer-events', 'none') .call( d3.axisBottom(this.xScale) - .ticks(this.options.axis.x.ticksCount) + .ticks(this.coreOptions.axis.x.ticksCount) .tickSize(DEFAULT_TICK_SIZE) - .tickFormat(this.getAxisTicksFormatter(this.options.axis.x)) + .tickFormat(this.getAxisTicksFormatter(this.coreOptions.axis.x)) ); - this.chartContainer.select('#x-axis-container').selectAll('.tick').selectAll('text') - .style('transform', this.xTickTransform); } protected renderYAxis(): void { - if(this.options.axis.y.isActive === false) { + if(this.coreOptions.axis.y.isActive === false) { return; } this.chartContainer.select('#y-axis-container').remove(); @@ -335,9 +259,9 @@ abstract class ChartwerkPod { // TODO: number of ticks shouldn't be hardcoded .call( d3.axisLeft(this.yScale) - .ticks(this.options.axis.y.ticksCount) + .ticks(this.coreOptions.axis.y.ticksCount) .tickSize(DEFAULT_TICK_SIZE) - .tickFormat(this.getAxisTicksFormatter(this.options.axis.y)) + .tickFormat(this.getAxisTicksFormatter(this.coreOptions.axis.y)) ); const ticks = this.yAxisElement.selectAll(`.tick`).select('text').nodes(); this.yAxisTicksColors.map((color, index) => { @@ -349,7 +273,7 @@ abstract class ChartwerkPod { } protected renderY1Axis(): void { - if(this.options.axis.y1.isActive === false) { + if(this.coreOptions.axis.y1.isActive === false) { return; } this.chartContainer.select('#y1-axis-container').remove(); @@ -363,7 +287,7 @@ abstract class ChartwerkPod { d3.axisRight(this.y1Scale) .ticks(DEFAULT_TICK_COUNT) .tickSize(DEFAULT_TICK_SIZE) - .tickFormat(this.getAxisTicksFormatter(this.options.axis.y1)) + .tickFormat(this.getAxisTicksFormatter(this.coreOptions.axis.y1)) ); } @@ -375,28 +299,28 @@ abstract class ChartwerkPod { .style('display', 'none'); if( - this.options.crosshair.orientation === CrosshairOrientation.VERTICAL || - this.options.crosshair.orientation === CrosshairOrientation.BOTH + this.coreOptions.crosshair.orientation === CrosshairOrientation.VERTICAL || + this.coreOptions.crosshair.orientation === CrosshairOrientation.BOTH ) { this.crosshair.append('line') .attr('class', 'crosshair-line') .attr('id', 'crosshair-line-x') - .attr('fill', this.options.crosshair.color) - .attr('stroke', this.options.crosshair.color) + .attr('fill', this.coreOptions.crosshair.color) + .attr('stroke', this.coreOptions.crosshair.color) .attr('stroke-width', '1px') .attr('y1', 0) .attr('y2', this.height) .style('pointer-events', 'none'); } if( - this.options.crosshair.orientation === CrosshairOrientation.HORIZONTAL || - this.options.crosshair.orientation === CrosshairOrientation.BOTH + this.coreOptions.crosshair.orientation === CrosshairOrientation.HORIZONTAL || + this.coreOptions.crosshair.orientation === CrosshairOrientation.BOTH ) { this.crosshair.append('line') .attr('class', 'crosshair-line') .attr('id', 'crosshair-line-y') - .attr('fill', this.options.crosshair.color) - .attr('stroke', this.options.crosshair.color) + .attr('fill', this.coreOptions.crosshair.color) + .attr('stroke', this.coreOptions.crosshair.color) .attr('stroke-width', '1px') .attr('x1', 0) .attr('x2', this.width) @@ -501,7 +425,7 @@ abstract class ChartwerkPod { this.initScaleX = this.xScale.copy(); this.initScaleY = this.yScale.copy(); - if(this.options.axis.y1.isActive === true) { + if(this.coreOptions.axis.y1.isActive) { this.initScaleY1 = this.y1Scale.copy(); } this.pan = d3.zoom() @@ -533,9 +457,6 @@ abstract class ChartwerkPod { .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) { @@ -549,11 +470,7 @@ abstract class ChartwerkPod { .attr('width', 13) .attr('height', 15) .html(`
`) - .on('click', () => { - if(this.options.eventsCallbacks !== undefined && this.options.eventsCallbacks.onLegendClick !== undefined) { - this.options.eventsCallbacks.onLegendClick(idx); - } - }); + .on('click', () => this.coreOptions.callbackLegendClick(idx)); legendRow.append('text') .attr('x', rowWidth + 20) @@ -562,16 +479,12 @@ abstract class ChartwerkPod { .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); - } - }); + .on('click', () => this.coreOptions.callbackLegendLabelClick(idx)); } } protected renderYLabel(): void { - if(this.options.labelFormat === undefined || this.options.labelFormat.yAxis === undefined) { + if(this.coreOptions.axis.y.label === undefined) { return; } this.chartContainer.append('text') @@ -583,11 +496,11 @@ abstract class ChartwerkPod { .style('text-anchor', 'middle') .style('font-size', '14px') .style('fill', 'currentColor') - .text(this.options.labelFormat.yAxis); + .text(this.coreOptions.axis.y.label); } protected renderXLabel(): void { - if(this.options.labelFormat === undefined || this.options.labelFormat.xAxis === undefined) { + if(this.coreOptions.axis.x.label === undefined) { return; } let yPosition = this.height + this.margin.top + this.margin.bottom - 35; @@ -601,7 +514,7 @@ abstract class ChartwerkPod { .style('text-anchor', 'middle') .style('font-size', '14px') .style('fill', 'currentColor') - .text(this.options.labelFormat.xAxis); + .text(this.coreOptions.axis.x.label); } protected renderNoDataPointsMessage(): void { @@ -883,9 +796,6 @@ abstract class ChartwerkPod { } protected zoomOut(): void { - if(this.isOutOfChart() === true) { - return; - } let xAxisMiddleValue: number = this.xScale.invert(this.width / 2); let yAxisMiddleValue: number = this.yScale.invert(this.height / 2); const centers = { @@ -999,52 +909,6 @@ abstract class ChartwerkPod { return MILISECONDS_IN_MINUTE; } - get xTickTransform(): string { - if(this.options.tickFormat === undefined || this.options.tickFormat.xTickOrientation === undefined) { - return ''; - } - switch (this.options.tickFormat.xTickOrientation) { - case TickOrientation.VERTICAL: - return 'translate(-10px, 50px) rotate(-90deg)'; - case TickOrientation.HORIZONTAL: - return ''; - case TickOrientation.DIAGONAL: - return 'translate(-30px, 30px) rotate(-45deg)'; - default: - return ''; - } - } - - get extraMargin(): Margin { - let optionalMargin = { top: 0, right: 0, bottom: 0, left: 0 }; - if(this.options.tickFormat !== undefined && this.options.tickFormat.xTickOrientation !== undefined) { - switch (this.options.tickFormat.xTickOrientation) { - case TickOrientation.VERTICAL: - optionalMargin.bottom += 80; - break; - case TickOrientation.HORIZONTAL: - break; - case TickOrientation.DIAGONAL: - optionalMargin.left += 15; - optionalMargin.bottom += 50; - optionalMargin.right += 10; - break; - } - } - if(this.options.labelFormat !== undefined) { - if(this.options.labelFormat.xAxis !== undefined && this.options.labelFormat.xAxis.length > 0) { - optionalMargin.bottom += 20; - } - if(this.options.labelFormat.yAxis !== undefined && this.options.labelFormat.yAxis.length > 0) { - optionalMargin.left += 20; - } - } - if(this.coreSeries.isSeriesAvailable) { - optionalMargin.bottom += 25; - } - return optionalMargin; - } - get width(): number { return this.d3Node.node().clientWidth - this.margin.left - this.margin.right; } @@ -1058,61 +922,25 @@ abstract class ChartwerkPod { } get margin(): Margin { - if(this.options.margin !== undefined) { - return this.options.margin; - } - return mergeWith({}, DEFAULT_MARGIN, this.extraMargin, add); - } - - formattedBound(alias: string, target: string): string { - const confidenceMetric = replace(alias, '$__metric_name', target); - return confidenceMetric; + return this.coreOptions.margin; } protected clearState(): void { this.state.clearState(); } - protected get seriesTargetsWithBounds(): any[] { - if( - this.options.bounds === undefined || - this.options.bounds.upper === undefined || - this.options.bounds.lower === undefined - ) { - return []; - } - let series = []; - 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 rectClipId(): string { if(this._clipPathUID.length === 0) { this._clipPathUID = uid(); } return this._clipPathUID; } - - isOutOfChart(): boolean { - const event = d3.mouse(this.chartContainer.node()); - const eventX = event[0]; - const eventY = event[1]; - if( - eventY > this.height + 1 || eventY < -1 || - eventX > this.width || eventX < 0 - ) { - return true; - } - return false; - } } export { ChartwerkPod, VueChartwerkPodMixin, - Margin, CoreSerie, Options, TickOrientation, TimeFormat, BrushOrientation, PanOrientation, + Margin, CoreSerie, Options, TimeFormat, BrushOrientation, PanOrientation, + AxesOptions, AxisOption, AxisFormat, yAxisOrientation, CrosshairOrientation, ScrollPanOrientation, ScrollPanDirection, KeyEvent, palette }; diff --git a/src/models/options.ts b/src/models/options.ts index e69de29..4bb935a 100644 --- a/src/models/options.ts +++ b/src/models/options.ts @@ -0,0 +1,166 @@ +import { + Options, + TickOrientation, + TimeFormat, + BrushOrientation, + AxisFormat, + CrosshairOrientation, + KeyEvent, + PanOrientation, + ScrollPanOrientation, + ScrollPanDirection, + GridOptions, + AxesOptions, + CrosshairOptions, + Margin +} from '../types'; + +import lodashDefaultsDeep from 'lodash/defaultsDeep'; +import lodashMap from 'lodash/map'; +import lodashCloneDeep from 'lodash/cloneDeep'; +import has from 'lodash/has'; + + +const DEFAULT_TICK_COUNT = 4; +const DEFAULT_SCROLL_PAN_STEP = 50; +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, + }, +} + +const DEFAULT_AXES_OPTIONS: AxesOptions = { + x: { + isActive: true, + ticksCount: DEFAULT_TICK_COUNT, + format: AxisFormat.TIME + }, + y: { + isActive: true, + ticksCount: DEFAULT_TICK_COUNT, + format: AxisFormat.NUMERIC + }, + y1: { + isActive: false, + ticksCount: DEFAULT_TICK_COUNT, + format: AxisFormat.NUMERIC + } +} + +const DEFAULT_CROSSHAIR_OPTIONS = { + orientation: CrosshairOrientation.VERTICAL, + color: 'gray', +} + +const DEFAULT_OPTIONS: Options = { + zoomEvents: { + mouse: { + zoom: { + isActive: true, + keyEvent: KeyEvent.MAIN, + orientation: BrushOrientation.HORIZONTAL + }, + pan: { + isActive: true, + keyEvent: KeyEvent.SHIFT, + orientation: PanOrientation.HORIZONTAL + }, + doubleClick: { + isActive: true, + keyEvent: KeyEvent.MAIN, + }, + }, + scroll: { + zoom: { + isActive: true, + keyEvent: KeyEvent.MAIN, + orientation: PanOrientation.BOTH, + }, + pan: { + isActive: false, + keyEvent: KeyEvent.SHIFT, + panStep: DEFAULT_SCROLL_PAN_STEP, + orientation: ScrollPanOrientation.HORIZONTAL, + direction: ScrollPanDirection.BOTH, + }, + }, + }, + axis: DEFAULT_AXES_OPTIONS, + grid: DEFAULT_GRID_OPTIONS, + crosshair: DEFAULT_CROSSHAIR_OPTIONS, + renderLegend: true, + // remove options below + renderTicksfromTimestamps: false, + timeInterval: { + timeFormat: TimeFormat.MINUTE + }, +} + +export class CoreOptions { + _options: O; + _defaults: Options = DEFAULT_OPTIONS; + + constructor(options: O) { + this.setOptions(options); + } + + public updateOptions(options: O): void { + this.setOptions(options); + } + + protected setOptions(options: O): void { + this._options = lodashDefaultsDeep(lodashCloneDeep(options), this._defaults); + } + + get allOptions(): O { + return this._options; + } + + get grid(): GridOptions { + return this._options.grid; + } + + get axis(): AxesOptions { + return this._options.axis; + } + + get crosshair(): CrosshairOptions { + return this._options.crosshair; + } + + get margin(): Margin { + return this._options.margin; + } + + // event callbacks + callbackRenderStart(): void { + if(has(this._options.eventsCallbacks, 'renderStart')) { + this._options.eventsCallbacks.renderStart(); + } + } + + callbackRenderEnd(): void { + if(has(this._options.eventsCallbacks, 'renderEnd')) { + this._options.eventsCallbacks.renderStart(); + } + } + + callbackLegendClick(idx: number): void { + if(has(this._options.eventsCallbacks, 'onLegendClick')) { + this._options.eventsCallbacks.onLegendClick(idx); + } + } + + callbackLegendLabelClick(idx: number): void { + if(has(this._options.eventsCallbacks, 'onLegendLabelClick')) { + this._options.eventsCallbacks.onLegendLabelClick(idx); + } + } +} diff --git a/src/models/series.ts b/src/models/series.ts index 9c2c37c..86dde81 100644 --- a/src/models/series.ts +++ b/src/models/series.ts @@ -9,6 +9,7 @@ import lodashUniq from 'lodash/uniq'; const SERIE_DEFAULTS = { alias: '', + target: '', visible: true, yOrientation: yAxisOrientation.LEFT, datapoints: [], @@ -19,6 +20,7 @@ const SERIE_DEFAULTS = { export class CoreSeries { _series: Array = []; + _defaults: CoreSerie = SERIE_DEFAULTS; constructor(series: T[]) { this.setSeries(series); @@ -42,7 +44,7 @@ export class CoreSeries { } protected fillDefaults(serie: T, idx: number): T { - let defaults = lodashCloneDeep(SERIE_DEFAULTS); + let defaults = lodashCloneDeep(this._defaults); defaults.color = palette[idx % palette.length]; defaults.idx = idx; lodashDefaultsDeep(serie, defaults); diff --git a/src/state.ts b/src/state.ts index 6d82239..75ee733 100755 --- a/src/state.ts +++ b/src/state.ts @@ -245,7 +245,7 @@ export class PodState { } protected filterSerieByYAxisOrientation(serie: T, orientation: yAxisOrientation): boolean { - if(serie.yOrientation === undefined || serie.yOrientation === yAxisOrientation.BOTH) { + if(serie.yOrientation === undefined) { return true; } return serie.yOrientation === orientation; diff --git a/src/types.ts b/src/types.ts index f16fa25..feaa196 100644 --- a/src/types.ts +++ b/src/types.ts @@ -16,7 +16,6 @@ export type CoreSerie = { // TODO: move some options to line-chart export type Options = { margin?: Margin; - confidence?: number; eventsCallbacks?: { zoomIn?: (range: AxisRange[]) => void, panning?: (event: { ranges: AxisRange[], d3Event: any }) => void, @@ -31,32 +30,13 @@ export type Options = { renderStart?: () => void, renderEnd?: () => void, }; - axis?: { - x?: AxisOption, - y?: AxisOption, - y1?: AxisOption - }; + axis?: AxesOptions; grid?: GridOptions; - crosshair?: { - orientation?: CrosshairOrientation; - color?: string; - } + crosshair?: CrosshairOptions; timeInterval?: { timeFormat?: TimeFormat; count?: number; }; - tickFormat?: { - xAxis?: string; - xTickOrientation?: TickOrientation; - }; - labelFormat?: { - xAxis?: string; - yAxis?: string; - }; - bounds?: { - upper: string; - lower: string; - }; timeRange?: { from: number, to: number @@ -96,6 +76,7 @@ export type Options = { renderTicksfromTimestamps?: boolean; renderLegend?: boolean; }; + export type GridOptions = { x?: { enabled?: boolean; @@ -106,22 +87,32 @@ export type GridOptions = { ticksCount?: number; }, } + +export type AxesOptions = { + x?: AxisOption, + y?: AxisOption, + y1?: AxisOption +} + export type AxisOption = { isActive?: boolean; ticksCount?: number; format?: AxisFormat; range?: [number, number]; invert?: boolean; + label?: string; valueFormatter?: (value: number, i: number) => string; colorFormatter?: (value: number, i: number) => string; } + +export type CrosshairOptions = { + orientation?: CrosshairOrientation; + color?: string; +} + export type AxisRange = [number, number] | undefined; export type VueOptions = Omit; -export enum TickOrientation { - VERTICAL = 'vertical', - HORIZONTAL = 'horizontal', - DIAGONAL = 'diagonal' -} + export enum TimeFormat { SECOND = 'second', MINUTE = 'minute', @@ -130,47 +121,56 @@ export enum TimeFormat { MONTH = 'month', YEAR = 'year' } + export enum BrushOrientation { VERTICAL = 'vertical', HORIZONTAL = 'horizontal', RECTANGLE = 'rectangle', SQUARE = 'square' } + export enum PanOrientation { VERTICAL = 'vertical', HORIZONTAL = 'horizontal', BOTH = 'both', } + export enum ScrollPanOrientation { VERTICAL = 'vertical', HORIZONTAL = 'horizontal', } + export enum ScrollPanDirection { FORWARD = 'forward', BACKWARD = 'backward', BOTH = 'both', } + export enum AxisFormat { TIME = 'time', NUMERIC = 'numeric', STRING = 'string', CUSTOM = 'custom' } + export enum CrosshairOrientation { VERTICAL = 'vertical', HORIZONTAL = 'horizontal', BOTH = 'both' } + export type SvgElementAttributes = { x: number, y: number, width: number, height: number } + export enum KeyEvent { MAIN = 'main', SHIFT = 'shift' } + // allow series values to affect a specific axis export enum xAxisOrientation { TOP = 'top', @@ -180,7 +180,6 @@ export enum xAxisOrientation { export enum yAxisOrientation { LEFT = 'left', RIGHT = 'right', - BOTH = 'both' } export type SvgElParams = { height: number,