|
|
|
@ -2,13 +2,15 @@ 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'; |
|
|
|
|
|
|
|
|
|
import { |
|
|
|
|
Margin, |
|
|
|
|
TimeSerie, |
|
|
|
|
CoreSerie, |
|
|
|
|
Options, |
|
|
|
|
TickOrientation, |
|
|
|
|
TimeFormat, |
|
|
|
|
BrushOrientation, |
|
|
|
|
AxisFormat, |
|
|
|
@ -20,96 +22,27 @@ import {
|
|
|
|
|
ScrollPanOrientation, |
|
|
|
|
ScrollPanDirection, |
|
|
|
|
AxisOption, |
|
|
|
|
AxesOptions, |
|
|
|
|
} from './types'; |
|
|
|
|
import { uid } from './utils'; |
|
|
|
|
import { palette } from './colors'; |
|
|
|
|
|
|
|
|
|
import * as d3 from 'd3'; |
|
|
|
|
|
|
|
|
|
import defaultsDeep from 'lodash/defaultsDeep'; |
|
|
|
|
import includes from 'lodash/includes'; |
|
|
|
|
import first from 'lodash/first'; |
|
|
|
|
import last from 'lodash/last'; |
|
|
|
|
import mergeWith from 'lodash/mergeWith'; |
|
|
|
|
import add from 'lodash/add'; |
|
|
|
|
import replace from 'lodash/replace'; |
|
|
|
|
import cloneDeep from 'lodash/cloneDeep'; |
|
|
|
|
import debounce from 'lodash/debounce'; |
|
|
|
|
import has from 'lodash/has'; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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<T extends TimeSerie, O extends Options> { |
|
|
|
|
abstract class ChartwerkPod<T extends CoreSerie, O extends Options> { |
|
|
|
|
|
|
|
|
|
protected coreSeries: CoreSeries<T>; |
|
|
|
|
protected coreOptions: CoreOptions<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 +63,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
|
|
|
|
@ -151,10 +83,8 @@ abstract class ChartwerkPod<T extends TimeSerie, O extends Options> {
|
|
|
|
|
// TODO: test if it's necessary
|
|
|
|
|
styles.use(); |
|
|
|
|
|
|
|
|
|
let options = cloneDeep(_options); |
|
|
|
|
defaultsDeep(options, DEFAULT_OPTIONS); |
|
|
|
|
this.options = options; |
|
|
|
|
this.series = cloneDeep(_series); |
|
|
|
|
this.coreOptions = new CoreOptions(_options); |
|
|
|
|
this.coreSeries = new CoreSeries(_series); |
|
|
|
|
|
|
|
|
|
this.d3Node = d3.select(this.el); |
|
|
|
|
this.addEventListeners(); |
|
|
|
@ -173,9 +103,7 @@ abstract class ChartwerkPod<T extends TimeSerie, O extends Options> {
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
public render(): void { |
|
|
|
|
if(has(this.options.eventsCallbacks, 'renderStart')) { |
|
|
|
|
this.options.eventsCallbacks.renderStart(); |
|
|
|
|
} |
|
|
|
|
this.coreOptions.callbackRenderStart(); |
|
|
|
|
|
|
|
|
|
this.renderClipPath(); |
|
|
|
|
this.addEvents(); |
|
|
|
@ -191,9 +119,7 @@ abstract class ChartwerkPod<T extends TimeSerie, O extends Options> {
|
|
|
|
|
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 { |
|
|
|
@ -217,18 +143,14 @@ abstract class ChartwerkPod<T extends TimeSerie, O extends Options> {
|
|
|
|
|
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 { |
|
|
|
|
if(newSeries === undefined) { |
|
|
|
|
return; |
|
|
|
|
} |
|
|
|
|
let series = cloneDeep(newSeries); |
|
|
|
|
this.series = series; |
|
|
|
|
this.coreSeries.updateSeries(newSeries); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
protected abstract renderMetrics(): void; |
|
|
|
@ -243,7 +165,8 @@ abstract class ChartwerkPod<T extends TimeSerie, O extends Options> {
|
|
|
|
|
height: this.height, |
|
|
|
|
width: this.width, |
|
|
|
|
} |
|
|
|
|
this.state = new PodState(boxPararms, this.series, this.options); |
|
|
|
|
// TODO: use instanses instead of oblects
|
|
|
|
|
this.state = new PodState(boxPararms, this.coreSeries.visibleSeries, this.coreOptions.allOptions); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
protected initComponents(): void { |
|
|
|
@ -256,7 +179,7 @@ abstract class ChartwerkPod<T extends TimeSerie, O extends Options> {
|
|
|
|
|
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 { |
|
|
|
@ -299,7 +222,7 @@ abstract class ChartwerkPod<T extends TimeSerie, O extends Options> {
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
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(); |
|
|
|
@ -310,16 +233,14 @@ abstract class ChartwerkPod<T extends TimeSerie, O extends Options> {
|
|
|
|
|
.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(); |
|
|
|
@ -331,9 +252,9 @@ abstract class ChartwerkPod<T extends TimeSerie, O extends Options> {
|
|
|
|
|
// 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) => { |
|
|
|
@ -345,7 +266,7 @@ abstract class ChartwerkPod<T extends TimeSerie, O extends Options> {
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
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(); |
|
|
|
@ -359,7 +280,7 @@ abstract class ChartwerkPod<T extends TimeSerie, O extends Options> {
|
|
|
|
|
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)) |
|
|
|
|
); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
@ -371,28 +292,28 @@ abstract class ChartwerkPod<T extends TimeSerie, O extends Options> {
|
|
|
|
|
.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) |
|
|
|
@ -402,8 +323,8 @@ abstract class ChartwerkPod<T extends TimeSerie, O extends Options> {
|
|
|
|
|
|
|
|
|
|
protected addEvents(): void { |
|
|
|
|
// TODO: refactor for a new mouse/scroll events
|
|
|
|
|
const panKeyEvent = this.options.zoomEvents.mouse.pan.keyEvent; |
|
|
|
|
const isPanActive = this.options.zoomEvents.mouse.pan.isActive; |
|
|
|
|
const panKeyEvent = this.coreOptions.mousePanEvent.keyEvent; |
|
|
|
|
const isPanActive = this.coreOptions.mousePanEvent.isActive; |
|
|
|
|
if(isPanActive === true && panKeyEvent === KeyEvent.MAIN) { |
|
|
|
|
this.initPan(); |
|
|
|
|
this.initBrush(); |
|
|
|
@ -420,11 +341,11 @@ abstract class ChartwerkPod<T extends TimeSerie, O extends Options> {
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
protected initBrush(): void { |
|
|
|
|
const isBrushActive = this.options.zoomEvents.mouse.zoom.isActive; |
|
|
|
|
const isBrushActive = this.coreOptions.mouseZoomEvent.isActive; |
|
|
|
|
if(isBrushActive === false) { |
|
|
|
|
return; |
|
|
|
|
} |
|
|
|
|
switch(this.options.zoomEvents.mouse.zoom.orientation) { |
|
|
|
|
switch(this.coreOptions.mouseZoomEvent.orientation) { |
|
|
|
|
case BrushOrientation.VERTICAL: |
|
|
|
|
this.brush = d3.brushY(); |
|
|
|
|
break; |
|
|
|
@ -438,7 +359,7 @@ abstract class ChartwerkPod<T extends TimeSerie, O extends Options> {
|
|
|
|
|
default: |
|
|
|
|
this.brush = d3.brushX(); |
|
|
|
|
} |
|
|
|
|
const keyEvent = this.options.zoomEvents.mouse.zoom.keyEvent; |
|
|
|
|
const keyEvent = this.coreOptions.mouseZoomEvent.keyEvent; |
|
|
|
|
this.brush.extent([ |
|
|
|
|
[0, 0], |
|
|
|
|
[this.width, this.height] |
|
|
|
@ -476,13 +397,13 @@ abstract class ChartwerkPod<T extends TimeSerie, O extends Options> {
|
|
|
|
|
|
|
|
|
|
protected initPan(): void { |
|
|
|
|
if( |
|
|
|
|
this.options.zoomEvents.mouse.pan.isActive === false && |
|
|
|
|
this.options.zoomEvents.scroll.pan.isActive === false && |
|
|
|
|
this.options.zoomEvents.scroll.zoom.isActive === false |
|
|
|
|
this.coreOptions.mousePanEvent.isActive === false && |
|
|
|
|
this.coreOptions.scrollPanEvent.isActive === false && |
|
|
|
|
this.coreOptions.scrollZoomEvent.isActive === false |
|
|
|
|
) { |
|
|
|
|
return; |
|
|
|
|
} |
|
|
|
|
if(this.options.zoomEvents.mouse.zoom.isActive === false) { |
|
|
|
|
if(this.coreOptions.mouseZoomEvent.isActive === false) { |
|
|
|
|
// init cumstom overlay to handle all events
|
|
|
|
|
this.customOverlay = this.chartContainer.append('rect') |
|
|
|
|
.attr('class', 'custom-overlay') |
|
|
|
@ -497,7 +418,7 @@ abstract class ChartwerkPod<T extends TimeSerie, O extends Options> {
|
|
|
|
|
|
|
|
|
|
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() |
|
|
|
@ -518,54 +439,45 @@ abstract class ChartwerkPod<T extends TimeSerie, O extends Options> {
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
protected renderLegend(): void { |
|
|
|
|
if(this.options.renderLegend === false) { |
|
|
|
|
if(this.coreOptions.allOptions.renderLegend === false) { |
|
|
|
|
return; |
|
|
|
|
} |
|
|
|
|
if(!this.coreSeries.isSeriesAvailable) { |
|
|
|
|
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; |
|
|
|
|
} |
|
|
|
|
const series = this.coreSeries.allSeries;
|
|
|
|
|
for(let idx = 0; idx < series.length; idx++) { |
|
|
|
|
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; |
|
|
|
|
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); |
|
|
|
|
} |
|
|
|
|
}); |
|
|
|
|
.on('click', () => this.coreOptions.callbackLegendClick(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); |
|
|
|
|
} |
|
|
|
|
}); |
|
|
|
|
} |
|
|
|
|
.style('fill', series[idx].color) |
|
|
|
|
.text(series[idx].target) |
|
|
|
|
.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') |
|
|
|
@ -577,15 +489,15 @@ abstract class ChartwerkPod<T extends TimeSerie, O extends Options> {
|
|
|
|
|
.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; |
|
|
|
|
if(this.series.length === 0) { |
|
|
|
|
if(this.coreSeries.isSeriesAvailable) { |
|
|
|
|
yPosition += 20; |
|
|
|
|
} |
|
|
|
|
this.chartContainer.append('text') |
|
|
|
@ -595,7 +507,7 @@ abstract class ChartwerkPod<T extends TimeSerie, O extends Options> {
|
|
|
|
|
.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 { |
|
|
|
@ -611,12 +523,12 @@ abstract class ChartwerkPod<T extends TimeSerie, O extends Options> {
|
|
|
|
|
|
|
|
|
|
private disableScrollForward(event: any): boolean { |
|
|
|
|
return event.sourceEvent.wheelDelta > 0 |
|
|
|
|
&& this.options.zoomEvents.scroll.pan.direction === ScrollPanDirection.FORWARD; |
|
|
|
|
&& this.coreOptions.scrollPanEvent.direction === ScrollPanDirection.FORWARD; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
private disableScrollBackward(event: any): boolean { |
|
|
|
|
return event.sourceEvent.wheelDelta < 0 |
|
|
|
|
&& this.options.zoomEvents.scroll.pan.direction === ScrollPanDirection.BACKWARD; |
|
|
|
|
&& this.coreOptions.scrollPanEvent.direction === ScrollPanDirection.BACKWARD; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
protected onPanning(): void { |
|
|
|
@ -630,14 +542,10 @@ abstract class ChartwerkPod<T extends TimeSerie, O extends Options> {
|
|
|
|
|
} |
|
|
|
|
this.rescaleMetricAndAxis(event); |
|
|
|
|
|
|
|
|
|
if(this.options.eventsCallbacks !== undefined && this.options.eventsCallbacks.panning !== undefined) { |
|
|
|
|
this.options.eventsCallbacks.panning({ |
|
|
|
|
this.coreOptions.callbackPanning({ |
|
|
|
|
ranges: [this.state.xValueRange, this.state.yValueRange, this.state.y1ValueRange], |
|
|
|
|
d3Event: event |
|
|
|
|
}); |
|
|
|
|
} else { |
|
|
|
|
console.log('on panning, but there is no callback'); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
public rescaleMetricAndAxis(event: d3.D3ZoomEvent<any, any>): void { |
|
|
|
@ -656,8 +564,8 @@ abstract class ChartwerkPod<T extends TimeSerie, O extends Options> {
|
|
|
|
|
protected onPanningRescale(event: d3.D3ZoomEvent<any, any>): void { |
|
|
|
|
// rescale metrics and axis on mouse and scroll panning
|
|
|
|
|
const eventType = event.sourceEvent.type; // 'wheel' or 'mousemove'
|
|
|
|
|
const scrollPanOptions = this.options.zoomEvents.scroll.pan; |
|
|
|
|
const scrollZoomOptions = this.options.zoomEvents.scroll.zoom; |
|
|
|
|
const scrollPanOptions = this.coreOptions.scrollPanEvent; |
|
|
|
|
const scrollZoomOptions = this.coreOptions.scrollZoomEvent; |
|
|
|
|
// TODO: maybe use switch and move it to onPanning
|
|
|
|
|
if(eventType === 'wheel') { |
|
|
|
|
if(scrollPanOptions.isActive === true && this.isD3EventKeyEqualOption(event, scrollPanOptions.keyEvent)) { |
|
|
|
@ -690,7 +598,7 @@ abstract class ChartwerkPod<T extends TimeSerie, O extends Options> {
|
|
|
|
|
return; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
const panOrientation = this.options.zoomEvents.mouse.pan.orientation; |
|
|
|
|
const panOrientation = this.coreOptions.mousePanEvent.orientation; |
|
|
|
|
if(panOrientation === PanOrientation.HORIZONTAL || panOrientation === PanOrientation.BOTH) { |
|
|
|
|
this.rescaleAxisX(event.transform.x); |
|
|
|
|
} |
|
|
|
@ -723,7 +631,7 @@ abstract class ChartwerkPod<T extends TimeSerie, O extends Options> {
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
protected onScrollPanningRescale(event: d3.D3ZoomEvent<any, any>): void { |
|
|
|
|
const scrollPanOptions = this.options.zoomEvents.scroll.pan; |
|
|
|
|
const scrollPanOptions = this.coreOptions.scrollPanEvent; |
|
|
|
|
// TODO: event.transform.y / x depends on mouse position, so we use hardcoded const, which should be removed
|
|
|
|
|
const transformStep = scrollPanOptions.panStep; |
|
|
|
|
const scrollPanOrientation = scrollPanOptions.orientation; |
|
|
|
@ -740,7 +648,7 @@ abstract class ChartwerkPod<T extends TimeSerie, O extends Options> {
|
|
|
|
|
case ScrollPanOrientation.VERTICAL: |
|
|
|
|
// @ts-ignore
|
|
|
|
|
let signY = Math.sign(event.transform.y); |
|
|
|
|
if(this.options.axis.y.invert === true) { |
|
|
|
|
if(this.coreOptions.axis.y.invert === true) { |
|
|
|
|
signY = -signY; |
|
|
|
|
} |
|
|
|
|
let rangeY = this.state.yValueRange; |
|
|
|
@ -764,16 +672,12 @@ abstract class ChartwerkPod<T extends TimeSerie, O extends Options> {
|
|
|
|
|
this.isPanning = false; |
|
|
|
|
this.deltaYTransform = 0; |
|
|
|
|
this.onMouseOut(); |
|
|
|
|
if(this.options.eventsCallbacks !== undefined && this.options.eventsCallbacks.panningEnd !== undefined) { |
|
|
|
|
this.options.eventsCallbacks.panningEnd([this.state.xValueRange, this.state.yValueRange, this.state.y1ValueRange]); |
|
|
|
|
} else { |
|
|
|
|
console.log('on panning end, but there is no callback'); |
|
|
|
|
} |
|
|
|
|
this.coreOptions.callbackPanningEnd([this.state.xValueRange, this.state.yValueRange, this.state.y1ValueRange]); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
protected onBrush(): void { |
|
|
|
|
const selection = d3.event.selection; |
|
|
|
|
if(this.options.zoomEvents.mouse.zoom.orientation !== BrushOrientation.SQUARE || selection === null) { |
|
|
|
|
if(this.coreOptions.mouseZoomEvent.orientation !== BrushOrientation.SQUARE || selection === null) { |
|
|
|
|
return; |
|
|
|
|
} |
|
|
|
|
const selectionAtts = this.getSelectionAttrs(selection); |
|
|
|
@ -829,7 +733,7 @@ abstract class ChartwerkPod<T extends TimeSerie, O extends Options> {
|
|
|
|
|
|
|
|
|
|
let xRange: [number, number]; |
|
|
|
|
let yRange: [number, number]; |
|
|
|
|
switch(this.options.zoomEvents.mouse.zoom.orientation) { |
|
|
|
|
switch(this.coreOptions.mouseZoomEvent.orientation) { |
|
|
|
|
case BrushOrientation.HORIZONTAL: |
|
|
|
|
const startTimestamp = this.xScale.invert(extent[0]); |
|
|
|
|
const endTimestamp = this.xScale.invert(extent[1]); |
|
|
|
@ -869,28 +773,17 @@ abstract class ChartwerkPod<T extends TimeSerie, O extends Options> {
|
|
|
|
|
this.brushStartSelection = null; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if(this.options.eventsCallbacks !== undefined && this.options.eventsCallbacks.zoomIn !== undefined) { |
|
|
|
|
this.options.eventsCallbacks.zoomIn([xRange, yRange]); |
|
|
|
|
} else { |
|
|
|
|
console.log('zoom in, but there is no callback'); |
|
|
|
|
} |
|
|
|
|
this.coreOptions.callbackZoomIn([xRange, yRange]); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
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 = { |
|
|
|
|
x: xAxisMiddleValue, |
|
|
|
|
y: yAxisMiddleValue |
|
|
|
|
} |
|
|
|
|
if(this.options.eventsCallbacks !== undefined && this.options.eventsCallbacks.zoomOut !== undefined) { |
|
|
|
|
this.options.eventsCallbacks.zoomOut(centers); |
|
|
|
|
} else { |
|
|
|
|
console.log('zoom out, but there is no callback'); |
|
|
|
|
} |
|
|
|
|
this.coreOptions.callbackZoomOut(centers); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// TODO: move to State
|
|
|
|
@ -921,10 +814,10 @@ abstract class ChartwerkPod<T extends TimeSerie, O extends Options> {
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
getd3TimeRangeEvery(count: number): d3.TimeInterval { |
|
|
|
|
if(this.options.timeInterval === undefined || this.options.timeInterval.timeFormat === undefined) { |
|
|
|
|
if(this.coreOptions.allOptions.timeInterval.timeFormat === undefined) { |
|
|
|
|
return d3.timeMinute.every(count); |
|
|
|
|
} |
|
|
|
|
switch(this.options.timeInterval.timeFormat) { |
|
|
|
|
switch(this.coreOptions.allOptions.timeInterval.timeFormat) { |
|
|
|
|
case TimeFormat.SECOND: |
|
|
|
|
return d3.utcSecond.every(count); |
|
|
|
|
case TimeFormat.MINUTE: |
|
|
|
@ -943,11 +836,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,63 +875,17 @@ 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) { |
|
|
|
|
if(this.coreOptions.allOptions.timeInterval.count !== undefined) { |
|
|
|
|
//TODO: timeFormat to timestamp
|
|
|
|
|
return this.options.timeInterval.count * MILISECONDS_IN_MINUTE; |
|
|
|
|
return this.coreOptions.allOptions.timeInterval.count * MILISECONDS_IN_MINUTE; |
|
|
|
|
} |
|
|
|
|
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.series.length > 0) { |
|
|
|
|
optionalMargin.bottom += 25; |
|
|
|
|
} |
|
|
|
|
return optionalMargin; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
get width(): number { |
|
|
|
|
return this.d3Node.node().clientWidth - this.margin.left - this.margin.right; |
|
|
|
|
} |
|
|
|
@ -1052,78 +899,25 @@ abstract class ChartwerkPod<T extends TimeSerie, O extends Options> {
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
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 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 || |
|
|
|
|
this.options.bounds.upper === undefined || |
|
|
|
|
this.options.bounds.lower === undefined |
|
|
|
|
) { |
|
|
|
|
return []; |
|
|
|
|
} |
|
|
|
|
let series = []; |
|
|
|
|
this.series.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(); |
|
|
|
|
} |
|
|
|
|
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, TimeSerie, Options, TickOrientation, TimeFormat, BrushOrientation, PanOrientation, |
|
|
|
|
Margin, CoreSerie, Options, TimeFormat, BrushOrientation, PanOrientation, |
|
|
|
|
AxesOptions, AxisOption, |
|
|
|
|
AxisFormat, yAxisOrientation, CrosshairOrientation, ScrollPanOrientation, ScrollPanDirection, KeyEvent, |
|
|
|
|
palette |
|
|
|
|
}; |
|
|
|
|