|
|
|
@ -1,10 +1,11 @@
|
|
|
|
|
import { ChartwerkPod, VueChartwerkPodMixin, TimeFormat, CrosshairOrientation, BrushOrientation, yAxisOrientation } from '@chartwerk/core'; |
|
|
|
|
import { LineTimeSerie, LineOptions, MouseObj } from './types'; |
|
|
|
|
import { LineTimeSerie, LineOptions, MouseObj, AreaType } from './types'; |
|
|
|
|
import { Markers } from './components/markers'; |
|
|
|
|
import { Segments } from './components/segments'; |
|
|
|
|
|
|
|
|
|
import { LineSeries } from './models/line_series'; |
|
|
|
|
import { MarkersConf } from './models/marker'; |
|
|
|
|
import { SegmentSerie } from './models/segment'; |
|
|
|
|
|
|
|
|
|
import * as d3 from 'd3'; |
|
|
|
|
import * as _ from 'lodash'; |
|
|
|
@ -13,14 +14,9 @@ const METRIC_CIRCLE_RADIUS = 1.5;
|
|
|
|
|
const CROSSHAIR_CIRCLE_RADIUS = 3; |
|
|
|
|
const CROSSHAIR_BACKGROUND_RAIDUS = 9; |
|
|
|
|
const CROSSHAIR_BACKGROUND_OPACITY = 0.3; |
|
|
|
|
|
|
|
|
|
type Generator = d3.Line<[number, number]> | d3.Area<[number, number]>; |
|
|
|
|
|
|
|
|
|
class LinePod extends ChartwerkPod<LineTimeSerie, LineOptions> { |
|
|
|
|
lineGenerator = null; |
|
|
|
|
areaGenerator = null; |
|
|
|
|
lineGeneratorY1 = null; |
|
|
|
|
areaGeneratorY1 = null; |
|
|
|
|
|
|
|
|
|
private _markersLayer: Markers = null; |
|
|
|
|
private _segmentsLayer: Segments = null; |
|
|
|
|
|
|
|
|
@ -28,8 +24,8 @@ class LinePod extends ChartwerkPod<LineTimeSerie, LineOptions> {
|
|
|
|
|
_el: HTMLElement, |
|
|
|
|
_series: LineTimeSerie[] = [], |
|
|
|
|
_options: LineOptions = {}, |
|
|
|
|
private _markersConf?: MarkersConf,
|
|
|
|
|
private _segmentSeries = [],
|
|
|
|
|
private _markersConf?: MarkersConf, |
|
|
|
|
private _segmentSeries: SegmentSerie[] = [], |
|
|
|
|
) { |
|
|
|
|
super(_el, _series, _options); |
|
|
|
|
this.series = new LineSeries(_series); |
|
|
|
@ -39,55 +35,70 @@ class LinePod extends ChartwerkPod<LineTimeSerie, LineOptions> {
|
|
|
|
|
this.clearAllMetrics(); |
|
|
|
|
|
|
|
|
|
this.updateCrosshair(); |
|
|
|
|
this.initLineGenerator(); |
|
|
|
|
this.initAreaGenerator(); |
|
|
|
|
this.updateEvents(); |
|
|
|
|
|
|
|
|
|
if(!this.series.isSeriesAvailable) { |
|
|
|
|
this.renderNoDataPointsMessage(); |
|
|
|
|
return; |
|
|
|
|
} |
|
|
|
|
for(const serie of this.series.visibleSeries) { |
|
|
|
|
this._renderMetric(serie); |
|
|
|
|
const generator = this.getRenderGenerator(serie.renderArea, serie.yOrientation); |
|
|
|
|
this._renderMetric(serie, generator); |
|
|
|
|
} |
|
|
|
|
if(this._markersConf !== undefined) { |
|
|
|
|
this._markersLayer = new Markers(this._markersConf, this.state); |
|
|
|
|
this._markersLayer.render(this.metricContainer); |
|
|
|
|
if(!_.isEmpty(this._markersConf)) { |
|
|
|
|
this._markersLayer = new Markers(this.d3Node, this._markersConf, this.state, this.margin); |
|
|
|
|
this._markersLayer.render(this.metricContainer, this.height); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if(!_.isEmpty(this._segmentSeries)) { |
|
|
|
|
this._segmentsLayer = new Segments(this._segmentSeries, this.state); |
|
|
|
|
this._segmentsLayer.render(this.metricContainer, this.chartContainer); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
this._segmentsLayer = new Segments(this._segmentSeries, this.state); |
|
|
|
|
this._segmentsLayer.render(this.metricContainer); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
clearAllMetrics(): void { |
|
|
|
|
// TODO: temporary hack before it will be implemented in core.
|
|
|
|
|
this.chartContainer.selectAll('.metric-el').remove(); |
|
|
|
|
this._markersLayer?.clear(); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
initLineGenerator(): void { |
|
|
|
|
this.lineGenerator = d3.line() |
|
|
|
|
.x(d => this.state.xScale(d[0])) |
|
|
|
|
.y(d => this.state.yScale(d[1])); |
|
|
|
|
this.lineGeneratorY1 = d3.line() |
|
|
|
|
.x(d => this.state.xScale(d[0])) |
|
|
|
|
.y(d => this.state.y1Scale(d[1])); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
initAreaGenerator(): void { |
|
|
|
|
this.areaGenerator = d3.area() |
|
|
|
|
.x(d => this.state.xScale(d[0])) |
|
|
|
|
.y1(d => this.state.yScale(d[1])) |
|
|
|
|
.y0(d => this.height); |
|
|
|
|
this.areaGeneratorY1 = d3.area() |
|
|
|
|
.x(d => this.state.xScale(d[0])) |
|
|
|
|
.y1(d => this.state.y1Scale(d[1])) |
|
|
|
|
.y0(d => this.height); |
|
|
|
|
protected updateEvents(): void { |
|
|
|
|
// overlay - core component that is used to handle mouse events
|
|
|
|
|
if(!this.overlay) { |
|
|
|
|
return; |
|
|
|
|
} |
|
|
|
|
if(this.options._options.events?.contextMenu === undefined) { |
|
|
|
|
return; |
|
|
|
|
} |
|
|
|
|
this.overlay.on('contextmenu', this.onContextMenu.bind(this)); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
getRenderGenerator(renderArea: boolean, yOrientation: yAxisOrientation): any { |
|
|
|
|
if(renderArea) { |
|
|
|
|
return yOrientation === yAxisOrientation.LEFT ? this.areaGenerator : this.areaGeneratorY1; |
|
|
|
|
getRenderGenerator(renderArea: AreaType, yOrientation: yAxisOrientation): Generator { |
|
|
|
|
const yScale = yOrientation === yAxisOrientation.LEFT ? this.state.yScale : this.state.y1Scale; |
|
|
|
|
const yValueRange = yOrientation === yAxisOrientation.LEFT ? this.state.yValueRange : this.state.y1ValueRange; |
|
|
|
|
const yAxisOptions = yOrientation === yAxisOrientation.LEFT ? this.options.axis.y : this.options.axis.y1; |
|
|
|
|
|
|
|
|
|
const topChartBorder = !yAxisOptions.invert ? yScale(yValueRange[1]) : yScale(yValueRange[0]); |
|
|
|
|
const bottomChartBorder = !yAxisOptions.invert ? yScale(yValueRange[0]) : yScale(yValueRange[1]); |
|
|
|
|
switch(renderArea) { |
|
|
|
|
case AreaType.NONE: |
|
|
|
|
// return line generator
|
|
|
|
|
return d3.line() |
|
|
|
|
.x(d => this.state.xScale(d[0])) |
|
|
|
|
.y(d => yScale(d[1])); |
|
|
|
|
case AreaType.ABOVE: |
|
|
|
|
return d3.area() |
|
|
|
|
.x(d => this.state.xScale(d[0])) |
|
|
|
|
.y0(topChartBorder) |
|
|
|
|
.y1(d => yScale(d[1])); |
|
|
|
|
case AreaType.BELOW: |
|
|
|
|
return d3.area() |
|
|
|
|
.x(d => this.state.xScale(d[0])) |
|
|
|
|
.y0(d => yScale(d[1])) |
|
|
|
|
.y1(bottomChartBorder); |
|
|
|
|
default: |
|
|
|
|
throw new Error(`Unknown type of renderArea: ${renderArea}`); |
|
|
|
|
} |
|
|
|
|
return yOrientation === yAxisOrientation.LEFT ? this.lineGenerator : this.areaGeneratorY1; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
_renderDots(serie: LineTimeSerie): void { |
|
|
|
@ -100,12 +111,12 @@ class LinePod extends ChartwerkPod<LineTimeSerie, LineOptions> {
|
|
|
|
|
.attr('r', METRIC_CIRCLE_RADIUS) |
|
|
|
|
.style('pointer-events', 'none') |
|
|
|
|
.attr('cx', d => this.state.xScale(d[0])) |
|
|
|
|
.attr('cy', d => this.state.yScale(d[1])); |
|
|
|
|
.attr('cy', d => this.state.getYScaleByOrientation(serie.yOrientation)(d[1])); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
_renderLines(serie: LineTimeSerie): void { |
|
|
|
|
const fillColor = serie.renderArea ? serie.color : 'none'; |
|
|
|
|
const fillOpacity = serie.renderArea ? 0.5 : 'none'; |
|
|
|
|
_renderLines(serie: LineTimeSerie, generator: Generator): void { |
|
|
|
|
const fillColor = serie.renderArea !== AreaType.NONE ? serie.color : 'none'; |
|
|
|
|
const fillOpacity = serie.renderArea !== AreaType.NONE ? 0.5 : 'none'; |
|
|
|
|
|
|
|
|
|
this.metricContainer |
|
|
|
|
.append('path') |
|
|
|
@ -118,12 +129,12 @@ class LinePod extends ChartwerkPod<LineTimeSerie, LineOptions> {
|
|
|
|
|
.attr('stroke-opacity', 0.7) |
|
|
|
|
.attr('pointer-events', 'none') |
|
|
|
|
.style('stroke-dasharray', serie.dashArray) |
|
|
|
|
.attr('d', this.getRenderGenerator(serie.renderArea, serie.yOrientation)); |
|
|
|
|
.attr('d', generator); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
_renderMetric(serie: LineTimeSerie): void { |
|
|
|
|
_renderMetric(serie: LineTimeSerie, generator: Generator): void { |
|
|
|
|
if(serie.renderLines === true) { |
|
|
|
|
this._renderLines(serie); |
|
|
|
|
this._renderLines(serie, generator); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if(serie.renderDots === true) { |
|
|
|
@ -167,23 +178,14 @@ class LinePod extends ChartwerkPod<LineTimeSerie, LineOptions> {
|
|
|
|
|
.style('pointer-events', 'none'); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
public renderSharedCrosshair(values: { x?: number, y?: number }): void { |
|
|
|
|
this.onMouseOver(); // TODO: refactor to use it once
|
|
|
|
|
const eventX = this.state.xScale(values.x); |
|
|
|
|
const eventY = this.state.yScale(values.y); |
|
|
|
|
this.moveCrosshairLine(eventX, eventY); |
|
|
|
|
const datapoints = this.findAndHighlightDatapoints(values.x, values.y); |
|
|
|
|
|
|
|
|
|
this.options.callbackSharedCrosshairMove({ |
|
|
|
|
datapoints: datapoints, |
|
|
|
|
eventX, eventY |
|
|
|
|
}); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
public hideSharedCrosshair(): void { |
|
|
|
|
this.crosshair.style('display', 'none'); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// TODO: refactor to make xPosition and yPosition optional
|
|
|
|
|
// and trough error if they are provided for wrong orientation
|
|
|
|
|
moveCrosshairLine(xPosition: number, yPosition: number): void { |
|
|
|
|
this.crosshair.style('display', null); |
|
|
|
|
switch(this.options.crosshair.orientation) { |
|
|
|
@ -291,12 +293,10 @@ class LinePod extends ChartwerkPod<LineTimeSerie, LineOptions> {
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
getMouseObj(): MouseObj { |
|
|
|
|
|
|
|
|
|
const eventX = d3.mouse(this.chartContainer.node())[0]; |
|
|
|
|
const eventY = d3.mouse(this.chartContainer.node())[1]; |
|
|
|
|
const xValue = this.state.xScale.invert(eventX); // mouse x position in xScale
|
|
|
|
|
const yValue = this.state.yScale.invert(eventY); |
|
|
|
|
this.moveCrosshairLine(eventX, eventY); |
|
|
|
|
|
|
|
|
|
const datapoints = this.findAndHighlightDatapoints(xValue, yValue); |
|
|
|
|
|
|
|
|
@ -311,19 +311,35 @@ class LinePod extends ChartwerkPod<LineTimeSerie, LineOptions> {
|
|
|
|
|
chartX: eventX, |
|
|
|
|
chartWidth: this.width |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
override onMouseMove(): void { |
|
|
|
|
const obj = this.getMouseObj(); |
|
|
|
|
const eventX = d3.mouse(this.chartContainer.node())[0]; |
|
|
|
|
const eventY = d3.mouse(this.chartContainer.node())[1]; |
|
|
|
|
|
|
|
|
|
this.moveCrosshairLine(eventX, eventY); |
|
|
|
|
|
|
|
|
|
// TODO: is shift key pressed
|
|
|
|
|
// TODO: need to refactor this object
|
|
|
|
|
this.options.callbackMouseMove(obj); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
public renderSharedCrosshair(values: { x?: number, y?: number }): void { |
|
|
|
|
this.showCrosshair(); |
|
|
|
|
this.moveCrosshairLine( |
|
|
|
|
values.x ? this.state.xScale(values.x) : 0, |
|
|
|
|
values.y ? this.state.yScale(values.y) : 0 |
|
|
|
|
); |
|
|
|
|
const datapoints = this.findAndHighlightDatapoints(values.x, values.y); |
|
|
|
|
|
|
|
|
|
this.options.callbackSharedCrosshairMove({ |
|
|
|
|
datapoints: datapoints, |
|
|
|
|
eventX: values.x ? this.state.xScale(values.x) : 0, |
|
|
|
|
eventY: values.y ? this.state.yScale(values.y) : 0 |
|
|
|
|
}); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
override onMouseClick(): void { |
|
|
|
|
this.options.callbackMouseClick(this.getMouseObj()); |
|
|
|
|
} |
|
|
|
@ -357,16 +373,26 @@ class LinePod extends ChartwerkPod<LineTimeSerie, LineOptions> {
|
|
|
|
|
return points; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
override onMouseOver(): void { |
|
|
|
|
this.onMouseMove(); |
|
|
|
|
showCrosshair() { |
|
|
|
|
this.crosshair.style('display', null); |
|
|
|
|
this.crosshair.selectAll('.crosshair-circle') |
|
|
|
|
.style('display', null); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
hideCrosshair() { |
|
|
|
|
this.crosshair.style('display', 'none'); |
|
|
|
|
this.crosshair.selectAll('.crosshair-circle') |
|
|
|
|
.style('display', 'none'); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
override onMouseOver(): void { |
|
|
|
|
this.showCrosshair(); |
|
|
|
|
this.onMouseMove(); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
override onMouseOut(): void { |
|
|
|
|
this.hideCrosshair(); |
|
|
|
|
this.options.callbackMouseOut(); |
|
|
|
|
this.crosshair.style('display', 'none'); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
isDoubleClickActive(): boolean { |
|
|
|
@ -378,6 +404,14 @@ class LinePod extends ChartwerkPod<LineTimeSerie, LineOptions> {
|
|
|
|
|
this.onMouseOver(); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
updateMarkers(markersConf: MarkersConf): void { |
|
|
|
|
this._markersConf = markersConf; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
updateSegments(segments: SegmentSerie[]): void { |
|
|
|
|
this._segmentSeries = segments; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// methods below rewrite s, (move more methods here)
|
|
|
|
|
protected zoomOut(): void { |
|
|
|
|
if(d3.event.type === 'dblclick' && !this.isDoubleClickActive()) { |
|
|
|
@ -419,7 +453,41 @@ class LinePod extends ChartwerkPod<LineTimeSerie, LineOptions> {
|
|
|
|
|
x: xAxisMiddleValue, |
|
|
|
|
y: yAxisMiddleValue |
|
|
|
|
} |
|
|
|
|
this.options.callbackZoomOut(centers); |
|
|
|
|
|
|
|
|
|
// TODO: refactor core not to take _options explicitly
|
|
|
|
|
if( |
|
|
|
|
this.options._options.events !== undefined && |
|
|
|
|
this.options._options.events.zoomOut !== undefined |
|
|
|
|
) { |
|
|
|
|
this.options._options.events.zoomOut( |
|
|
|
|
centers, |
|
|
|
|
[this.state.xValueRange, this.state.yValueRange] |
|
|
|
|
); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
protected onContextMenu(): void { |
|
|
|
|
d3.event.preventDefault(); // do not open browser's context menu.
|
|
|
|
|
const eventX = d3.mouse(this.chartContainer.node())[0]; |
|
|
|
|
const eventY = d3.mouse(this.chartContainer.node())[1]; |
|
|
|
|
|
|
|
|
|
this.options._options.events.contextMenu({ |
|
|
|
|
x: this.state.xScale.invert(eventX), |
|
|
|
|
y: this.state.yScale.invert(eventY), |
|
|
|
|
}); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// override parent updateData method to provide markers and segments
|
|
|
|
|
protected updateLineData( |
|
|
|
|
series?: LineTimeSerie[], |
|
|
|
|
options?: LineOptions, |
|
|
|
|
markersConf?: MarkersConf, |
|
|
|
|
segments?: SegmentSerie[], |
|
|
|
|
shouldRerender = true |
|
|
|
|
): void { |
|
|
|
|
this.updateMarkers(markersConf); |
|
|
|
|
this.updateSegments(segments); |
|
|
|
|
this.updateData(series, options, shouldRerender); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
@ -427,6 +495,26 @@ class LinePod extends ChartwerkPod<LineTimeSerie, LineOptions> {
|
|
|
|
|
// it is used with Vue.component, e.g.: Vue.component('chartwerk-line-pod', VueChartwerkLinePod)
|
|
|
|
|
export const VueChartwerkLinePod = { |
|
|
|
|
// alternative to `template: '<div class="chartwerk-line-pod" :id="id" />'`
|
|
|
|
|
props: { |
|
|
|
|
markersConf: { |
|
|
|
|
type: Object, |
|
|
|
|
required: false, |
|
|
|
|
default: function() { return {}; } |
|
|
|
|
}, |
|
|
|
|
segments: { |
|
|
|
|
type: Array, |
|
|
|
|
required: false, |
|
|
|
|
default: function() { return []; } |
|
|
|
|
}, |
|
|
|
|
}, |
|
|
|
|
watch: { |
|
|
|
|
markersConf() { |
|
|
|
|
this.renderChart(); |
|
|
|
|
}, |
|
|
|
|
segments() { |
|
|
|
|
this.renderChart(); |
|
|
|
|
}, |
|
|
|
|
}, |
|
|
|
|
render(createElement) { |
|
|
|
|
return createElement( |
|
|
|
|
'div', |
|
|
|
@ -440,10 +528,10 @@ export const VueChartwerkLinePod = {
|
|
|
|
|
methods: { |
|
|
|
|
render() { |
|
|
|
|
if(this.pod === undefined) { |
|
|
|
|
this.pod = new LinePod(document.getElementById(this.id), this.series, this.options); |
|
|
|
|
this.pod = new LinePod(document.getElementById(this.id), this.series, this.options, this.markersConf, this.segments); |
|
|
|
|
this.pod.render(); |
|
|
|
|
} else { |
|
|
|
|
this.pod.updateData(this.series, this.options); |
|
|
|
|
this.pod.updateLineData(this.series, this.options, this.markersConf, this.segments); |
|
|
|
|
} |
|
|
|
|
}, |
|
|
|
|
renderSharedCrosshair(values) { |
|
|
|
@ -455,4 +543,4 @@ export const VueChartwerkLinePod = {
|
|
|
|
|
} |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
export { LineTimeSerie, LineOptions, TimeFormat, LinePod }; |
|
|
|
|
export { LineTimeSerie, LineOptions, TimeFormat, LinePod, AreaType, MarkersConf, SegmentSerie }; |
|
|
|
|