|
|
@ -1,5 +1,5 @@ |
|
|
|
import { ChartwerkPod, VueChartwerkPodMixin, TimeFormat, CrosshairOrientation, BrushOrientation, yAxisOrientation } from '@chartwerk/core'; |
|
|
|
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 { Markers } from './components/markers'; |
|
|
|
import { Segments } from './components/segments'; |
|
|
|
import { Segments } from './components/segments'; |
|
|
|
|
|
|
|
|
|
|
@ -14,14 +14,9 @@ const METRIC_CIRCLE_RADIUS = 1.5; |
|
|
|
const CROSSHAIR_CIRCLE_RADIUS = 3; |
|
|
|
const CROSSHAIR_CIRCLE_RADIUS = 3; |
|
|
|
const CROSSHAIR_BACKGROUND_RAIDUS = 9; |
|
|
|
const CROSSHAIR_BACKGROUND_RAIDUS = 9; |
|
|
|
const CROSSHAIR_BACKGROUND_OPACITY = 0.3; |
|
|
|
const CROSSHAIR_BACKGROUND_OPACITY = 0.3; |
|
|
|
|
|
|
|
type Generator = d3.Line<[number, number]> | d3.Area<[number, number]>; |
|
|
|
|
|
|
|
|
|
|
|
class LinePod extends ChartwerkPod<LineTimeSerie, LineOptions> { |
|
|
|
class LinePod extends ChartwerkPod<LineTimeSerie, LineOptions> { |
|
|
|
lineGenerator = null; |
|
|
|
|
|
|
|
areaGenerator = null; |
|
|
|
|
|
|
|
lineGeneratorY1 = null; |
|
|
|
|
|
|
|
areaGeneratorY1 = null; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private _markersLayer: Markers = null; |
|
|
|
private _markersLayer: Markers = null; |
|
|
|
private _segmentsLayer: Segments = null; |
|
|
|
private _segmentsLayer: Segments = null; |
|
|
|
|
|
|
|
|
|
|
@ -40,55 +35,70 @@ class LinePod extends ChartwerkPod<LineTimeSerie, LineOptions> { |
|
|
|
this.clearAllMetrics(); |
|
|
|
this.clearAllMetrics(); |
|
|
|
|
|
|
|
|
|
|
|
this.updateCrosshair(); |
|
|
|
this.updateCrosshair(); |
|
|
|
this.initLineGenerator(); |
|
|
|
this.updateEvents(); |
|
|
|
this.initAreaGenerator(); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if(!this.series.isSeriesAvailable) { |
|
|
|
if(!this.series.isSeriesAvailable) { |
|
|
|
this.renderNoDataPointsMessage(); |
|
|
|
this.renderNoDataPointsMessage(); |
|
|
|
return; |
|
|
|
return; |
|
|
|
} |
|
|
|
} |
|
|
|
for(const serie of this.series.visibleSeries) { |
|
|
|
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) { |
|
|
|
if(!_.isEmpty(this._markersConf)) { |
|
|
|
this._markersLayer = new Markers(this._markersConf, this.state); |
|
|
|
this._markersLayer = new Markers(this.d3Node, this._markersConf, this.state, this.margin); |
|
|
|
this._markersLayer.render(this.metricContainer); |
|
|
|
this._markersLayer.render(this.metricContainer, this.height); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
this._segmentsLayer = new Segments(this._segmentSeries, this.state); |
|
|
|
if(!_.isEmpty(this._segmentSeries)) { |
|
|
|
this._segmentsLayer.render(this.metricContainer); |
|
|
|
this._segmentsLayer = new Segments(this._segmentSeries, this.state); |
|
|
|
|
|
|
|
this._segmentsLayer.render(this.metricContainer, this.chartContainer); |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
clearAllMetrics(): void { |
|
|
|
clearAllMetrics(): void { |
|
|
|
// TODO: temporary hack before it will be implemented in core.
|
|
|
|
// TODO: temporary hack before it will be implemented in core.
|
|
|
|
this.chartContainer.selectAll('.metric-el').remove(); |
|
|
|
this.chartContainer.selectAll('.metric-el').remove(); |
|
|
|
|
|
|
|
this._markersLayer?.clear(); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
initLineGenerator(): void { |
|
|
|
protected updateEvents(): void { |
|
|
|
this.lineGenerator = d3.line() |
|
|
|
// overlay - core component that is used to handle mouse events
|
|
|
|
.x(d => this.state.xScale(d[0])) |
|
|
|
if(!this.overlay) { |
|
|
|
.y(d => this.state.yScale(d[1])); |
|
|
|
return; |
|
|
|
this.lineGeneratorY1 = d3.line() |
|
|
|
} |
|
|
|
.x(d => this.state.xScale(d[0])) |
|
|
|
if(this.options._options.events?.contextMenu === undefined) { |
|
|
|
.y(d => this.state.y1Scale(d[1])); |
|
|
|
return; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
this.overlay.on('contextmenu', this.onContextMenu.bind(this)); |
|
|
|
initAreaGenerator(): void { |
|
|
|
} |
|
|
|
this.areaGenerator = d3.area() |
|
|
|
|
|
|
|
.x(d => this.state.xScale(d[0])) |
|
|
|
getRenderGenerator(renderArea: AreaType, yOrientation: yAxisOrientation): Generator { |
|
|
|
.y1(d => this.state.yScale(d[1])) |
|
|
|
const yScale = yOrientation === yAxisOrientation.LEFT ? this.state.yScale : this.state.y1Scale; |
|
|
|
.y0(d => this.height); |
|
|
|
const yValueRange = yOrientation === yAxisOrientation.LEFT ? this.state.yValueRange : this.state.y1ValueRange; |
|
|
|
this.areaGeneratorY1 = d3.area() |
|
|
|
const yAxisOptions = yOrientation === yAxisOrientation.LEFT ? this.options.axis.y : this.options.axis.y1; |
|
|
|
.x(d => this.state.xScale(d[0])) |
|
|
|
|
|
|
|
.y1(d => this.state.y1Scale(d[1])) |
|
|
|
const topChartBorder = !yAxisOptions.invert ? yScale(yValueRange[1]) : yScale(yValueRange[0]); |
|
|
|
.y0(d => this.height); |
|
|
|
const bottomChartBorder = !yAxisOptions.invert ? yScale(yValueRange[0]) : yScale(yValueRange[1]); |
|
|
|
} |
|
|
|
switch(renderArea) { |
|
|
|
|
|
|
|
case AreaType.NONE: |
|
|
|
getRenderGenerator(renderArea: boolean, yOrientation: yAxisOrientation): any { |
|
|
|
// return line generator
|
|
|
|
if(renderArea) { |
|
|
|
return d3.line() |
|
|
|
return yOrientation === yAxisOrientation.LEFT ? this.areaGenerator : this.areaGeneratorY1; |
|
|
|
.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 { |
|
|
|
_renderDots(serie: LineTimeSerie): void { |
|
|
@ -101,12 +111,12 @@ class LinePod extends ChartwerkPod<LineTimeSerie, LineOptions> { |
|
|
|
.attr('r', METRIC_CIRCLE_RADIUS) |
|
|
|
.attr('r', METRIC_CIRCLE_RADIUS) |
|
|
|
.style('pointer-events', 'none') |
|
|
|
.style('pointer-events', 'none') |
|
|
|
.attr('cx', d => this.state.xScale(d[0])) |
|
|
|
.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 { |
|
|
|
_renderLines(serie: LineTimeSerie, generator: Generator): void { |
|
|
|
const fillColor = serie.renderArea ? serie.color : 'none'; |
|
|
|
const fillColor = serie.renderArea !== AreaType.NONE ? serie.color : 'none'; |
|
|
|
const fillOpacity = serie.renderArea ? 0.5 : 'none'; |
|
|
|
const fillOpacity = serie.renderArea !== AreaType.NONE ? 0.5 : 'none'; |
|
|
|
|
|
|
|
|
|
|
|
this.metricContainer |
|
|
|
this.metricContainer |
|
|
|
.append('path') |
|
|
|
.append('path') |
|
|
@ -119,12 +129,12 @@ class LinePod extends ChartwerkPod<LineTimeSerie, LineOptions> { |
|
|
|
.attr('stroke-opacity', 0.7) |
|
|
|
.attr('stroke-opacity', 0.7) |
|
|
|
.attr('pointer-events', 'none') |
|
|
|
.attr('pointer-events', 'none') |
|
|
|
.style('stroke-dasharray', serie.dashArray) |
|
|
|
.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) { |
|
|
|
if(serie.renderLines === true) { |
|
|
|
this._renderLines(serie); |
|
|
|
this._renderLines(serie, generator); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
if(serie.renderDots === true) { |
|
|
|
if(serie.renderDots === true) { |
|
|
@ -455,12 +465,56 @@ class LinePod extends ChartwerkPod<LineTimeSerie, LineOptions> { |
|
|
|
); |
|
|
|
); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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); |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// TODO: it should be moved to VUE folder
|
|
|
|
// TODO: it should be moved to VUE folder
|
|
|
|
// it is used with Vue.component, e.g.: Vue.component('chartwerk-line-pod', VueChartwerkLinePod)
|
|
|
|
// it is used with Vue.component, e.g.: Vue.component('chartwerk-line-pod', VueChartwerkLinePod)
|
|
|
|
export const VueChartwerkLinePod = { |
|
|
|
export const VueChartwerkLinePod = { |
|
|
|
// alternative to `template: '<div class="chartwerk-line-pod" :id="id" />'`
|
|
|
|
// 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) { |
|
|
|
render(createElement) { |
|
|
|
return createElement( |
|
|
|
return createElement( |
|
|
|
'div', |
|
|
|
'div', |
|
|
@ -474,10 +528,10 @@ export const VueChartwerkLinePod = { |
|
|
|
methods: { |
|
|
|
methods: { |
|
|
|
render() { |
|
|
|
render() { |
|
|
|
if(this.pod === undefined) { |
|
|
|
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(); |
|
|
|
this.pod.render(); |
|
|
|
} else { |
|
|
|
} else { |
|
|
|
this.pod.updateData(this.series, this.options); |
|
|
|
this.pod.updateLineData(this.series, this.options, this.markersConf, this.segments); |
|
|
|
} |
|
|
|
} |
|
|
|
}, |
|
|
|
}, |
|
|
|
renderSharedCrosshair(values) { |
|
|
|
renderSharedCrosshair(values) { |
|
|
@ -489,4 +543,4 @@ export const VueChartwerkLinePod = { |
|
|
|
} |
|
|
|
} |
|
|
|
}; |
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
export { LineTimeSerie, LineOptions, TimeFormat, LinePod }; |
|
|
|
export { LineTimeSerie, LineOptions, TimeFormat, LinePod, AreaType, MarkersConf, SegmentSerie }; |
|
|
|