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