diff --git a/examples/demo_live.html b/examples/demo_live.html
index a8f80dd..072a77c 100755
--- a/examples/demo_live.html
+++ b/examples/demo_live.html
@@ -15,9 +15,9 @@
const startTime = 1590590148;
const arrayLength = 100;
this.isZoomed = false; // TODO: temporary hack to have zoomin|zoomout with `appendData`. It will be moved to Pod.
- const data1 = Array.from({ length: arrayLength }, (el, idx) => [startTime + idx * 10000, Math.floor(Math.random() * 40)]);
- const data2 = Array.from({ length: arrayLength }, (el, idx) => [startTime + idx * 10000, Math.floor(Math.random() * 100)]);
- const data3 = Array.from({ length: arrayLength }, (el, idx) => [startTime + idx * 10000, Math.floor(Math.random() * 20) + 90]);
+ var data1 = Array.from({ length: arrayLength }, (el, idx) => [startTime + idx * 10000, Math.floor(Math.random() * 40)]);
+ var data2 = Array.from({ length: arrayLength }, (el, idx) => [startTime + idx * 10000, Math.floor(Math.random() * 100)]);
+ var data3 = Array.from({ length: arrayLength }, (el, idx) => [startTime + idx * 10000, Math.floor(Math.random() * 20) + 90]);
const zoomIn = (ranges) => { const xRange = ranges[0]; options.axis.x.range = xRange; pod.updateData(undefined, options); this.isZoomed = true }
const zoomOut = (ranges) => { options.axis.x.range = undefined; pod.updateData(undefined, options); this.isZoomed = false }
let options = { renderLegend: false, axis: { y: { invert: false, range: [0, 350] }, x: { format: 'time' } }, eventsCallbacks: { zoomIn: zoomIn, zoomOut } };
@@ -37,9 +37,17 @@
const d1 = [startTime + rerenderIdx * 10000, Math.floor(Math.random() * 20) + 90];
const d2 = [startTime + rerenderIdx * 10000, Math.floor(Math.random() * 100)];
const d3 = [startTime + rerenderIdx * 10000, Math.floor(Math.random() * 20) + 90];
+ console.log('d1', data1)
const shouldRerender = !this.isZoomed;
console.time('rerender');
- pod.appendData([d1, d2, d3], shouldRerender);
+ data1.push(d1);
+ data2.push(d2);
+ data3.push(d3);
+ pod.updateData([
+ { target: 'test1', datapoints: data1, color: 'green', maxLength: arrayLength + 30, renderDots: false },
+ { target: 'test2', datapoints: data2, color: 'blue', maxLength: arrayLength + 30, renderDots: false },
+ { target: 'test3', datapoints: data3, color: 'orange', maxLength: arrayLength + 30, renderDots: false },
+ ]);
console.timeEnd('rerender');
if(rerenderIdx > arrayLength + 100) {
clearInterval(test);
diff --git a/package.json b/package.json
index 4f5d0e5..30e7724 100644
--- a/package.json
+++ b/package.json
@@ -6,7 +6,8 @@
"scripts": {
"build": "webpack --config build/webpack.prod.conf.js && webpack --config build/webpack.dev.conf.js",
"dev": "webpack --watch --config build/webpack.dev.conf.js",
- "test": "echo \"Error: no test specified\" && exit 1"
+ "test": "echo \"Error: no test specified\" && exit 1",
+ "update-core": "yarn up @chartwerk/core && yarn up @chartwerk/core@latest"
},
"repository": {
"type": "git",
diff --git a/src/index.ts b/src/index.ts
index 97917cf..70ec9c5 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,5 +1,7 @@
-import { ChartwerkPod, VueChartwerkPodMixin, TickOrientation, TimeFormat, CrosshairOrientation, BrushOrientation } from '@chartwerk/core';
-import { LineTimeSerie, LineOptions, Mode } from './types';
+import { ChartwerkPod, VueChartwerkPodMixin, TimeFormat, CrosshairOrientation, BrushOrientation } from '@chartwerk/core';
+import { LineTimeSerie, LineOptions } from './types';
+
+import { LineSeries } from './models/line_series';
import * as d3 from 'd3';
import * as _ from 'lodash';
@@ -15,6 +17,7 @@ export class LinePod extends ChartwerkPod {
constructor(_el: HTMLElement, _series: LineTimeSerie[] = [], _options: LineOptions = {}) {
super(_el, _series, _options);
+ this.series = new LineSeries(_series);
}
renderMetrics(): void {
@@ -24,35 +27,13 @@ export class LinePod extends ChartwerkPod {
this.initLineGenerator();
this.initAreaGenerator();
- // TODO: seems that renderMetrics is not correct name
- if(this.series.length === 0) {
+ if(!this.series.isSeriesAvailable) {
this.renderNoDataPointsMessage();
return;
}
- for(let idx = 0; idx < this.series.length; ++idx) {
- if(this.series[idx].visible === false) {
- continue;
- }
- // TODO: use _.defaults same as in core
- const confidence = this.series[idx].confidence || 0;
- const mode = this.series[idx].mode || Mode.STANDARD;
- const target = this.series[idx].target;
- const renderDots = this.series[idx].renderDots !== undefined ? this.series[idx].renderDots : false;
- const renderLines = this.series[idx].renderLines !== undefined ? this.series[idx].renderLines : true;
-
- this._renderMetric(
- this.series[idx].datapoints,
- {
- color: this.getSerieColor(idx),
- confidence,
- target,
- mode,
- serieIdx: idx,
- renderDots,
- renderLines,
- }
- );
+ for(const serie of this.series.visibleSeries) {
+ this._renderMetric(serie);
}
}
@@ -63,141 +44,62 @@ export class LinePod extends ChartwerkPod {
initLineGenerator(): void {
this.lineGenerator = d3.line()
- .x(d => this.xScale(d[0]))
- .y(d => this.yScale(d[1]));
+ .x(d => this.state.xScale(d[0]))
+ .y(d => this.state.yScale(d[1]));
}
initAreaGenerator(): void {
this.areaGenerator = d3.area()
- .x(d => this.xScale(d[0]))
- .y1(d => this.yScale(d[1]))
+ .x(d => this.state.xScale(d[0]))
+ .y1(d => this.state.yScale(d[1]))
.y0(d => this.height);
}
- getRenderGenerator(serieIdx: number): any {
- const renderArea = this.series[serieIdx].renderArea;
+ getRenderGenerator(renderArea: boolean): any {
if(renderArea) {
return this.areaGenerator;
}
return this.lineGenerator;
}
- public appendData(data: [number, number][], shouldRerender = true): void {
- for(let idx = 0; idx < this.series.length; ++idx) {
- if(this.series[idx].visible === false) {
- continue;
- }
- this.series[idx].datapoints.push(data[idx]);
- const maxLength = this.series[idx].maxLength;
- if(maxLength !== undefined && this.series[idx].datapoints.length > maxLength) {
- this.series[idx].datapoints.shift();
- }
- }
-
- for(let idx = 0; idx < this.series.length; ++idx) {
- this.metricContainer.select(`.metric-path-${idx}`)
- .datum(this.series[idx].datapoints)
- .attr('d', this.getRenderGenerator(idx));
-
- if(this.series[idx].renderDots === true) {
- this.metricContainer.selectAll(`.metric-circle-${idx}`)
- .data(this.series[idx].datapoints)
- .attr('cx', d => this.xScale(d[0]))
- .attr('cy', d => this.yScale(d[1]));
-
- this._renderDots([data[idx]], idx);
- }
- }
- if(shouldRerender) {
- const rightBorder = _.last(data)[0];
- this.state.xValueRange = [this.state.getMinValueX(), rightBorder];
- this.renderXAxis();
- this.renderYAxis();
- this.renderGrid();
- }
- }
-
- _renderDots(datapoints: number[][], serieIdx: number): void {
- const customClass = this.series[serieIdx].class || '';
-
+ _renderDots(serie: LineTimeSerie): void {
this.metricContainer.selectAll(null)
- .data(datapoints)
+ .data(serie.datapoints)
.enter()
.append('circle')
- .attr('class', `metric-circle-${serieIdx} metric-el ${customClass}`)
- .attr('fill', this.getSerieColor(serieIdx))
+ .attr('class', `metric-circle-${serie.idx} metric-el ${serie.class}`)
+ .attr('fill', serie.color)
.attr('r', METRIC_CIRCLE_RADIUS)
.style('pointer-events', 'none')
- .attr('cx', d => this.xScale(d[0]))
- .attr('cy', d => this.yScale(d[1]));
+ .attr('cx', d => this.state.xScale(d[0]))
+ .attr('cy', d => this.state.yScale(d[1]));
}
- _renderLines(datapoints: number[][], serieIdx: number): void {
- const dashArray = this.series[serieIdx].dashArray !== undefined ? this.series[serieIdx].dashArray : '0';
- const customClass = this.series[serieIdx].class || '';
- const fillColor = this.series[serieIdx].renderArea ? this.getSerieColor(serieIdx) : 'none';
- const fillOpacity = this.series[serieIdx].renderArea ? 0.5 : 'none';
+ _renderLines(serie: LineTimeSerie): void {
+ const fillColor = serie.renderArea ? serie.color : 'none';
+ const fillOpacity = serie.renderArea ? 0.5 : 'none';
this.metricContainer
.append('path')
- .datum(datapoints)
- .attr('class', `metric-path-${serieIdx} metric-el ${customClass}`)
+ .datum(serie.datapoints)
+ .attr('class', `metric-path-${serie.idx} metric-el ${serie.class}`)
.attr('fill', fillColor)
.attr('fill-opacity', fillOpacity)
- .attr('stroke', this.getSerieColor(serieIdx))
+ .attr('stroke', serie.color)
.attr('stroke-width', 1)
.attr('stroke-opacity', 0.7)
.attr('pointer-events', 'none')
- .style('stroke-dasharray', dashArray)
- .attr('d', this.getRenderGenerator(serieIdx));
+ .style('stroke-dasharray', serie.dashArray)
+ .attr('d', this.getRenderGenerator(serie.renderArea));
}
- _renderMetric(
- datapoints: number[][],
- metricOptions: {
- color: string,
- confidence: number,
- target: string,
- mode: Mode,
- serieIdx: number,
- renderDots: boolean,
- renderLines: boolean,
- }
- ): void {
- if(_.includes(this.seriesTargetsWithBounds, metricOptions.target)) {
- return;
- }
-
- if(metricOptions.mode === Mode.CHARGE) {
- const dataPairs = d3.pairs(datapoints);
- this.metricContainer.selectAll(null)
- .data(dataPairs)
- .enter()
- .append('line')
- .attr('x1', d => this.xScale(d[0][0]))
- .attr('x2', d => this.xScale(d[1][0]))
- .attr('y1', d => this.yScale(d[0][1]))
- .attr('y2', d => this.yScale(d[1][1]))
- .attr('stroke-opacity', 0.7)
- .style('stroke-width', 1)
- .style('stroke', d => {
- if(d[1][0] > d[0][0]) {
- return 'green';
- } else if (d[1][0] < d[0][0]) {
- return 'red';
- } else {
- return 'gray';
- }
- });
- return;
- }
-
- if(metricOptions.renderLines === true) {
- this._renderLines(datapoints, metricOptions.serieIdx);
+ _renderMetric(serie: LineTimeSerie): void {
+ if(serie.renderLines === true) {
+ this._renderLines(serie);
}
- if(metricOptions.renderDots === true) {
- this._renderDots(datapoints, metricOptions.serieIdx);
+ if(serie.renderDots === true) {
+ this._renderDots(serie);
}
}
@@ -209,7 +111,7 @@ export class LinePod extends ChartwerkPod {
appendCrosshairCircles(): void {
// circle for each serie
- this.series.forEach((serie: LineTimeSerie, serieIdx: number) => {
+ this.series.visibleSeries.forEach((serie: LineTimeSerie, serieIdx: number) => {
this.appendCrosshairCircle(serieIdx);
});
}
@@ -222,7 +124,7 @@ export class LinePod extends ChartwerkPod {
.attr('cx', -CROSSHAIR_BACKGROUND_RAIDUS)
.attr('cy', -CROSSHAIR_BACKGROUND_RAIDUS)
.attr('clip-path', `url(#${this.rectClipId})`)
- .attr('fill', this.getSerieColor(serieIdx))
+ .attr('fill', this.series.visibleSeries[serieIdx].color)
.style('opacity', CROSSHAIR_BACKGROUND_OPACITY)
.style('pointer-events', 'none');
@@ -232,23 +134,19 @@ export class LinePod extends ChartwerkPod {
.attr('cy', -CROSSHAIR_CIRCLE_RADIUS)
.attr('class', `crosshair-circle-${serieIdx}`)
.attr('clip-path', `url(#${this.rectClipId})`)
- .attr('fill', this.getSerieColor(serieIdx))
+ .attr('fill', this.series.visibleSeries[serieIdx].color)
.attr('r', CROSSHAIR_CIRCLE_RADIUS)
.style('pointer-events', 'none');
}
public renderSharedCrosshair(values: { x?: number, y?: number }): void {
this.onMouseOver(); // TODO: refactor to use it once
- const eventX = this.xScale(values.x);
- const eventY = this.yScale(values.y);
+ 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);
- if(this.options.eventsCallbacks === undefined || this.options.eventsCallbacks.sharedCrosshairMove === undefined) {
- return;
- }
-
- this.options.eventsCallbacks.sharedCrosshairMove({
+ this.options.callbackSharedCrosshairMove({
datapoints: datapoints,
eventX, eventY
});
@@ -350,7 +248,7 @@ export class LinePod extends ChartwerkPod {
// columnIdx: 1 for y, 0 for x
// inverval: x/y value interval between data points
// TODO: move it to base/state instead of timeInterval
- const intervals = _.map(this.series, serie => {
+ const intervals = _.map(this.series.visibleSeries, serie => {
if(serie.datapoints.length < 2) {
return undefined;
}
@@ -366,23 +264,15 @@ export class LinePod extends ChartwerkPod {
onMouseMove(): void {
const eventX = d3.mouse(this.chartContainer.node())[0];
const eventY = d3.mouse(this.chartContainer.node())[1];
- const xValue = this.xScale.invert(eventX); // mouse x position in xScale
- const yValue = this.yScale.invert(eventY);
- // TODO: isOutOfChart is a hack, use clip path correctly
- if(this.isOutOfChart() === true) {
- this.crosshair.style('display', 'none');
- return;
- }
+ 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);
- if(this.options.eventsCallbacks === undefined || this.options.eventsCallbacks.mouseMove === undefined) {
- return;
- }
// TDOO: is shift key pressed
// TODO: need to refactor this object
- this.options.eventsCallbacks.mouseMove({
+ this.options.callbackMouseMove({
x: d3.event.pageX,
y: d3.event.pageY,
xVal: xValue,
@@ -394,67 +284,30 @@ export class LinePod extends ChartwerkPod {
}
findAndHighlightDatapoints(xValue: number, yValue: number): { value: [number, number], color: string, label: string }[] {
- if(this.series === undefined || this.series.length === 0) {
+ if(!this.series.isSeriesAvailable) {
return [];
}
let points = []; // datapoints in each metric that is closest to xValue/yValue position
- this.series.forEach((serie: LineTimeSerie, serieIdx: number) => {
- if(
- serie.visible === false ||
- _.includes(this.seriesTargetsWithBounds, serie.target)
- ) {
- this.hideCrosshairCircle(serieIdx);
- return;
- }
+ this.series.visibleSeries.forEach((serie: LineTimeSerie) => {
const closestDatapoint = this.getClosestDatapoint(serie, xValue, yValue);
- if(closestDatapoint === undefined || this.isOutOfRange(closestDatapoint, xValue, yValue, serie.useOutOfRange)) {
- this.hideCrosshairCircle(serieIdx);
+ if(closestDatapoint === undefined) {
+ this.hideCrosshairCircle(serie.idx);
return;
}
- const xPosition = this.xScale(closestDatapoint[0]);
- const yPosition = this.yScale(closestDatapoint[1]);
- this.moveCrosshairCircle(xPosition, yPosition, serieIdx);
+ const xPosition = this.state.xScale(closestDatapoint[0]);
+ const yPosition = this.state.yScale(closestDatapoint[1]);
+ this.moveCrosshairCircle(xPosition, yPosition, serie.idx);
points.push({
value: closestDatapoint,
- color: this.getSerieColor(serieIdx),
+ color: serie.color,
label: serie.alias || serie.target
});
});
return points;
}
- isOutOfRange(closestDatapoint: [number, number], xValue: number, yValue: number, useOutOfRange = true): boolean {
- // find is mouse position more than xRange/yRange from closest point
- // TODO: refactor getValueInterval to remove this!
- if(useOutOfRange === false) {
- return false;
- }
- let columnIdx; // 1 for y value, 0 for x value
- let value; // xValue ot y Value
- switch(this.options.crosshair.orientation) {
- case CrosshairOrientation.VERTICAL:
- columnIdx = 0;
- value = xValue;
- break;
- case CrosshairOrientation.HORIZONTAL:
- columnIdx = 1;
- value = yValue;
- break;
- case CrosshairOrientation.BOTH:
- // TODO: maybe use voronoi
- columnIdx = 1;
- value = yValue;
- default:
- throw new Error(`Unknown type of crosshair orientaion: ${this.options.crosshair.orientation}`);
- }
- const range = Math.abs(closestDatapoint[columnIdx] - value);
- const interval = this.getValueInterval(columnIdx); // interval between points
- // do not move crosshair circles, it mouse to far from closest point
- return interval === undefined || range > interval / 2;
- }
-
onMouseOver(): void {
this.crosshair.style('display', null);
this.crosshair.selectAll('.crosshair-circle')
@@ -462,30 +315,21 @@ export class LinePod extends ChartwerkPod {
}
onMouseOut(): void {
- if(this.options.eventsCallbacks !== undefined && this.options.eventsCallbacks.mouseOut !== undefined) {
- this.options.eventsCallbacks.mouseOut();
- }
+ this.options.callbackMouseOut();
this.crosshair.style('display', 'none');
}
isDoubleClickActive(): boolean {
- if(this.options.zoomEvents.mouse.doubleClick === undefined) {
- return false;
- }
- return this.options.zoomEvents.mouse.doubleClick.isActive;
+ return this.options.doubleClickEvent.isActive;
}
- // methods below rewrite cores, (move more methods here)
+ // methods below rewrite s, (move more methods here)
protected zoomOut(): void {
- // TODO: test to remove, seems its depricated
- if(this.isOutOfChart() === true) {
- return;
- }
if(d3.event.type === 'dblclick' && !this.isDoubleClickActive()) {
return;
}
// TODO: its not clear, why we use this orientation here. Mb its better to use separate option
- const orientation: BrushOrientation = this.options.zoomEvents.mouse.zoom.orientation;
+ const orientation: BrushOrientation = this.options.mouseZoomEvent.orientation;
const xInterval = this.state.xValueRange[1] - this.state.xValueRange[0];
const yInterval = this.state.yValueRange[1] - this.state.yValueRange[0];
switch(orientation) {
@@ -514,15 +358,13 @@ export class LinePod extends ChartwerkPod {
this.renderGrid();
this.onMouseOver();
- if(this.options.eventsCallbacks !== undefined && this.options.eventsCallbacks.zoomOut !== undefined) {
- let xAxisMiddleValue: number = this.xScale.invert(this.width / 2);
- let yAxisMiddleValue: number = this.yScale.invert(this.height / 2);
- const centers = {
- x: xAxisMiddleValue,
- y: yAxisMiddleValue
- }
- this.options.eventsCallbacks.zoomOut(centers);
+ let xAxisMiddleValue: number = this.state.xScale.invert(this.width / 2);
+ let yAxisMiddleValue: number = this.state.yScale.invert(this.height / 2);
+ const centers = {
+ x: xAxisMiddleValue,
+ y: yAxisMiddleValue
}
+ this.options.callbackZoomOut(centers);
}
}
@@ -557,4 +399,4 @@ export const VueChartwerkLinePod = {
}
};
-export { LineTimeSerie, LineOptions, Mode, TickOrientation, TimeFormat };
+export { LineTimeSerie, LineOptions, TimeFormat };
diff --git a/src/models/line_series.ts b/src/models/line_series.ts
new file mode 100644
index 0000000..0b779dd
--- /dev/null
+++ b/src/models/line_series.ts
@@ -0,0 +1,23 @@
+import { CoreSeries } from '@chartwerk/core';
+import { LineTimeSerie } from '../types';
+
+
+const LINE_SERIE_DEFAULTS = {
+ maxLength: undefined,
+ renderDots: false,
+ renderLines: true,
+ dashArray: '0',
+ class: '',
+ renderArea: false,
+};
+
+export class LineSeries extends CoreSeries {
+
+ constructor(series: LineTimeSerie[]) {
+ super(series);
+ }
+
+ protected get defaults(): LineTimeSerie {
+ return { ...this._coreDefaults, ...LINE_SERIE_DEFAULTS };
+ }
+}
diff --git a/src/types.ts b/src/types.ts
index 1575cfe..5924894 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -1,19 +1,13 @@
-import { TimeSerie, Options } from '@chartwerk/core';
+import { Serie, Options } from '@chartwerk/core';
type LineTimeSerieParams = {
- confidence: number,
- mode: Mode,
maxLength: number,
renderDots: boolean,
renderLines: boolean, // TODO: refactor same as scatter-pod
- useOutOfRange: boolean, // It's temporary hack. Need to refactor getValueInterval() method
dashArray: string; // dasharray attr, only for lines
class: string; // option to add custom class to each serie element
renderArea: boolean; // TODO: move to render type
}
-export enum Mode {
- STANDARD = 'Standard',
- CHARGE = 'Charge'
-}
-export type LineTimeSerie = TimeSerie & Partial;
+
+export type LineTimeSerie = Serie & Partial;
export type LineOptions = Options;
diff --git a/yarn.lock b/yarn.lock
index 167871d..4aec6d8 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -6,12 +6,12 @@ __metadata:
cacheKey: 8
"@chartwerk/core@npm:latest":
- version: 0.5.4
- resolution: "@chartwerk/core@npm:0.5.4"
+ version: 0.6.0
+ resolution: "@chartwerk/core@npm:0.6.0"
dependencies:
d3: ^5.7.2
lodash: ^4.14.149
- checksum: 0d686409377d6880f0012db3d9c1e1910cf3660885ac13196dabe93f57eca53984cf52dcf781b2876373fe9efc7b9d0209117cbcd811c50892b1de4f38a4a37f
+ checksum: db368ea1ba1f9b48583c3da953998206329ea552eaff4e9f13cb02b25e0289464eca268baa86f045d62552a0859fc3ee2ccf70e7df95b58327bba217f9888169
languageName: node
linkType: hard