Browse Source

Merge branch 'new-mouse-move-callback-format' into 'main'

update delaunay data with serie target && update mouse move callback type

See merge request chartwerk/scatter-pod!6
main
Alexander Velikiy 2 years ago
parent
commit
f49b3d404d
  1. 109
      src/index.ts
  2. 27
      src/models/delaunay.ts
  3. 6
      src/models/scatter_series.ts
  4. 24
      src/types.ts

109
src/index.ts

@ -1,7 +1,7 @@
import { ChartwerkPod, VueChartwerkPodMixin, TimeFormat, yAxisOrientation, CrosshairOrientation } from '@chartwerk/core'; import { ChartwerkPod, VueChartwerkPodMixin, TimeFormat, yAxisOrientation, CrosshairOrientation } from '@chartwerk/core';
import { ScatterData, ScatterOptions, PointType, LineType } from './types'; import { ScatterData, ScatterOptions, PointType, LineType, HighlightedData, MouseMoveEvent } from './types';
import { DelaunayDiagram } from './models/delaunay'; import { DelaunayDiagram, DelaunayDataRow } from './models/delaunay';
import { ScatterSeries } from './models/scatter_series'; import { ScatterSeries } from './models/scatter_series';
import * as d3 from 'd3'; import * as d3 from 'd3';
@ -14,6 +14,7 @@ const DEFAULT_LINE_DASHED_AMOUNT = 4;
export class ChartwerkScatterPod extends ChartwerkPod<ScatterData, ScatterOptions> { export class ChartwerkScatterPod extends ChartwerkPod<ScatterData, ScatterOptions> {
metricContainer: any; metricContainer: any;
_delaunayDiagram: DelaunayDiagram; _delaunayDiagram: DelaunayDiagram;
series: ScatterSeries;
constructor(el: HTMLElement, _series: ScatterData[] = [], _options: ScatterOptions = {}) { constructor(el: HTMLElement, _series: ScatterData[] = [], _options: ScatterOptions = {}) {
super(el, _series, _options); super(el, _series, _options);
@ -27,7 +28,7 @@ export class ChartwerkScatterPod extends ChartwerkPod<ScatterData, ScatterOption
} }
this.updateCrosshair(); this.updateCrosshair();
this._delaunayDiagram = new DelaunayDiagram(this.series.visibleSeries, this.state.xScale, this.getYScale.bind(this)); this._delaunayDiagram = new DelaunayDiagram(this.series, this.state.xScale, this.getYScale.bind(this));
this.renderLines(); this.renderLines();
this.renderPoints(); this.renderPoints();
@ -115,41 +116,35 @@ export class ChartwerkScatterPod extends ChartwerkPod<ScatterData, ScatterOption
if(!this._delaunayDiagram.data) { if(!this._delaunayDiagram.data) {
return; return;
} }
this.metricContainer.selectAll(null) this.metricContainer.selectAll(null)
.data(this._delaunayDiagram.data) .data(this._delaunayDiagram.data)
.enter() .enter()
.append('circle') .append('circle')
.filter((d: number[]) => this.getSerieByIdx(_.last(d))?.pointType !== PointType.RECTANGLE) .filter((d: DelaunayDataRow) => this.series.getSerieByTarget(d[4])?.pointType !== PointType.RECTANGLE)
.attr('class', (d, i: number) => `metric-element metric-circle point-${i}`) .attr('class', (d, i: number) => `metric-element metric-circle point-${i}`)
.attr('r', (d: number[]) => this.getSerieByIdx(_.last(d))?.pointSize) .attr('r', (d: DelaunayDataRow) => this.series.getSerieByTarget(d[4])?.pointSize)
.style('fill', (d: number[], i: number) => this.getSeriesColorFromDataRow(d, i)) .style('fill', (d: DelaunayDataRow, i: number) => this.getSeriesColorFromDataRow(d, i))
.style('pointer-events', 'none') .style('pointer-events', 'none')
.attr('cx', (d: any[]) => this.state.xScale(d[0])) .attr('cx', (d: DelaunayDataRow) => this.state.xScale(d[0]))
.attr('cy', (d: any[]) => this.getYScale(this.getSerieByIdx(_.last(d))?.yOrientation)(d[1])); .attr('cy', (d: DelaunayDataRow) => this.getYScale(this.series.getSerieByTarget(d[4])?.yOrientation)(d[1]));
this.metricContainer.selectAll(null) this.metricContainer.selectAll(null)
.data(this._delaunayDiagram.data) .data(this._delaunayDiagram.data)
.enter() .enter()
.append('rect') .append('rect')
.filter((d: number[]) => this.getSerieByIdx(_.last(d))?.pointType === PointType.RECTANGLE) .filter((d: DelaunayDataRow) => this.series.getSerieByTarget(d[4])?.pointType === PointType.RECTANGLE)
.attr('class', (d, i: number) => `metric-element metric-circle point-${i}`) .attr('class', (d, i: number) => `metric-element metric-circle point-${i}`)
.attr('r', (d: number[]) => this.getSerieByIdx(_.last(d))?.pointSize) .attr('r', (d: DelaunayDataRow) => this.series.getSerieByTarget(d[4])?.pointSize)
.style('fill', (d: number[]) => this.getSerieByIdx(_.last(d))?.color) .style('fill', (d: DelaunayDataRow) => this.series.getSerieByTarget(d[4])?.color)
.style('pointer-events', 'none') .style('pointer-events', 'none')
.attr('x', (d: number[]) => this.state.xScale(d[0]) - this.getSerieByIdx(_.last(d))?.pointSize / 2) .attr('x', (d: DelaunayDataRow) => this.state.xScale(d[0]) - this.series.getSerieByTarget(d[4])?.pointSize / 2)
.attr('y', (d: number[]) => this.getYScale(this.getSerieByIdx(_.last(d)).yOrientation)(d[1]) - this.getSerieByIdx(_.last(d))?.pointSize / 2) .attr('y', (d: DelaunayDataRow) => this.getYScale(this.series.getSerieByTarget(d[4])?.yOrientation)(d[1]) - this.series.getSerieByTarget(d[4])?.pointSize / 2)
.attr('width', (d: number[]) => this.getSerieByIdx(_.last(d))?.pointSize) .attr('width', (d: DelaunayDataRow) => this.series.getSerieByTarget(d[4])?.pointSize)
.attr('height', (d: number[]) => this.getSerieByIdx(_.last(d))?.pointSize); .attr('height', (d: DelaunayDataRow) => this.series.getSerieByTarget(d[4])?.pointSize);
}
getSerieByIdx(idx: number): ScatterData | undefined {
return _.find(this.series.visibleSeries, serie => serie.idx === idx);
} }
getSeriesColorFromDataRow(values: number[], rowIdx: number): string { getSeriesColorFromDataRow(values: DelaunayDataRow, rowIdx: number): string {
const seriesIdx = _.last(values); const serie = this.series.getSerieByTarget(values[4]);
const serie = this.getSerieByIdx(seriesIdx);
if(serie?.colorFormatter) { if(serie?.colorFormatter) {
return serie.colorFormatter(values, rowIdx); return serie.colorFormatter(values, rowIdx);
} }
@ -170,20 +165,18 @@ export class ChartwerkScatterPod extends ChartwerkPod<ScatterData, ScatterOption
highlight(pointIdx: number): void { highlight(pointIdx: number): void {
this.unhighlight(); this.unhighlight();
const datapoint = this._delaunayDiagram.getDataRowByIndex(pointIdx); const row = this._delaunayDiagram.getDataRowByIndex(pointIdx);
if(datapoint === undefined || datapoint === null) { if(row === undefined || row === null) {
return; return;
} }
const serie = this.series.getSerieByTarget(row[4]);
const serieIdx = _.last(datapoint);
const serie = this.getSerieByIdx(serieIdx);
const size = this.getCrosshairCircleBackgroundSize(serie.pointSize, serie.pointType); const size = this.getCrosshairCircleBackgroundSize(serie.pointSize, serie.pointType);
this.crosshair.selectAll(`.crosshair-point-${serieIdx}`) this.crosshair.selectAll(`.crosshair-point-${serie.idx}`)
.attr('cx', this.state.xScale(datapoint[0])) .attr('cx', this.state.xScale(row[0]))
.attr('cy', this.getYScale(serie.yOrientation)(datapoint[1])) .attr('cy', this.getYScale(serie.yOrientation)(row[1]))
.attr('x', this.state.xScale(datapoint[0]) - size / 2) .attr('x', this.state.xScale(row[0]) - size / 2)
.attr('y', this.getYScale(serie.yOrientation)(datapoint[1]) - size / 2) .attr('y', this.getYScale(serie.yOrientation)(row[1]) - size / 2)
.attr('fill', this.getSeriesColorFromDataRow(datapoint, pointIdx)) .attr('fill', this.getSeriesColorFromDataRow(row, pointIdx))
.style('display', null); .style('display', null);
} }
@ -233,21 +226,35 @@ export class ChartwerkScatterPod extends ChartwerkPod<ScatterData, ScatterOption
} }
} }
findAndHighlightDatapoints(eventX: number, eventY: number): { values: any[], pointIdx: number } | null { findAndHighlightDatapoints(eventX: number, eventY: number): {
xValue: number, yValue: number, customValue: number,
pointIdx: number, totalPointIdx: number,
serieInfo: { target: string, alias?: string, class?: string, idx?: number }
} | null {
if(!this.series.isSeriesAvailable) { if(!this.series.isSeriesAvailable) {
return null; return null;
} }
const pointIndex = this._delaunayDiagram.findPointIndex(eventX, eventY); const pointIndex = this._delaunayDiagram.findPointIndex(eventX, eventY);
if(pointIndex === undefined) { if(pointIndex === undefined) {
this.unhighlight(); this.unhighlight();
return null; return null;
} }
this.highlight(pointIndex); this.highlight(pointIndex);
const row = this._delaunayDiagram.data[pointIndex];
const serie = this.series.getSerieByTarget(row[4]);
return { return {
values: this._delaunayDiagram.data[pointIndex], xValue: row[0],
pointIdx: pointIndex, yValue: row[1],
customValue: row[2],
pointIdx: row[3],
serieInfo: {
target: serie.target,
alias: serie.alias,
class: serie.class,
idx: serie.idx,
},
totalPointIdx: pointIndex,
}; };
} }
@ -282,16 +289,20 @@ export class ChartwerkScatterPod extends ChartwerkPod<ScatterData, ScatterOption
// TOOD: it should be two different methods // TOOD: it should be two different methods
const highlighted = this.findAndHighlightDatapoints(eventX, eventY); const highlighted = this.findAndHighlightDatapoints(eventX, eventY);
// TODO: group fields
this.options.callbackMouseMove({ this.options.callbackMouseMove({
x: d3.event.clientX, bbox: {
y: d3.event.clientY, clientX: d3.event.clientX,
xval: this.state.xScale.invert(eventX), clientY: d3.event.clientY,
yval: this.state.xScale.invert(eventY), x: eventX,
highlighted, y: eventY,
chartX: eventX, chartWidth: this.width,
chartWidth: this.width chartHeight: this.height,
},
data: {
xval: this.state.xScale.invert(eventX),
yval: this.state.xScale.invert(eventY),
highlighted,
}
}); });
} }
@ -334,4 +345,4 @@ export const VueChartwerkScatterPodObject = {
} }
}; };
export { ScatterData, ScatterOptions, TimeFormat, PointType, LineType }; export { ScatterData, ScatterOptions, TimeFormat, PointType, LineType, HighlightedData, MouseMoveEvent };

27
src/models/delaunay.ts

@ -1,24 +1,30 @@
import { ScatterData, PointType } from '../types'; import { ScatterData, PointType } from '../types';
import { ScatterSeries } from './scatter_series';
import { Delaunay } from 'd3-delaunay'; import { Delaunay } from 'd3-delaunay';
import * as _ from 'lodash'; import * as _ from 'lodash';
import * as d3 from 'd3'; // only types import * as d3 from 'd3'; // only types
// return type row: [ 0:x, 1:y, 2:(custom value | null), 3:pointIdx, 4:serie.target ]
type Value = number;
type PointIdx = number;
type Target = string;
export type DelaunayDataRow = [Value, Value, Value | null, PointIdx, Target];
export class DelaunayDiagram { export class DelaunayDiagram {
private _delaunayData: number[][]; // [ 0:y, 1:x, ..., last:serieIdx ][] private _delaunayData: DelaunayDataRow[];
private _delaunayDiagram: any; private _delaunayDiagram: any;
constructor( constructor(
protected series: ScatterData[], protected series: ScatterSeries,
xScale: d3.ScaleLinear<number, number>, yScale: (string) => d3.ScaleLinear<number, number>, // TODO: bad, but idk how to do it better xScale: d3.ScaleLinear<number, number>, yScale: (string) => d3.ScaleLinear<number, number>, // TODO: bad, but idk how to do it better
) { ) {
this._delaunayData = this.getDatapointsForDelaunay(); this._delaunayData = this.getDatapointsForDelaunay();
this.setDelaunayDiagram(xScale, yScale); this.setDelaunayDiagram(xScale, yScale);
} }
public get data(): number[][] | undefined { public get data(): DelaunayDataRow[] | undefined {
if(!this._delaunayData || this._delaunayData.length === 0) { if(!this._delaunayData || this._delaunayData.length === 0) {
return undefined; return undefined;
} }
@ -33,8 +39,8 @@ export class DelaunayDiagram {
console.time('delaunay-init'); console.time('delaunay-init');
this._delaunayDiagram = Delaunay.from( this._delaunayDiagram = Delaunay.from(
this._delaunayData, this._delaunayData,
(d: number[]) => xScale(d[0]), (d: DelaunayDataRow) => xScale(d[0]),
(d: number[]) => yScale(this.series[_.last(d)].yOrientation)(d[1]), (d: DelaunayDataRow) => yScale(this.series.getSerieByTarget(d[4])?.yOrientation)(d[1]),
); );
console.timeEnd('delaunay-init'); console.timeEnd('delaunay-init');
} }
@ -51,28 +57,27 @@ export class DelaunayDiagram {
return pointIndex; return pointIndex;
} }
public getDataRowByIndex(index: number): number[] | undefined { public getDataRowByIndex(index: number): DelaunayDataRow | undefined {
if(!this.data) { if(!this.data) {
return undefined; return undefined;
} }
return this.data[index]; return this.data[index];
} }
private getDatapointsForDelaunay(): number[][] | undefined { private getDatapointsForDelaunay(): DelaunayDataRow[] | undefined {
// here we union all datapoints with point render type(circle or rectangle) // here we union all datapoints with point render type(circle or rectangle)
// it means that circles and rectangles will be highlighted(not lines) // it means that circles and rectangles will be highlighted(not lines)
// TODO: set Defaults (if pointType === undefined, Circle type will be used futher) // TODO: set Defaults (if pointType === undefined, Circle type will be used futher)
const seriesForPointType = this.series.filter((serie: ScatterData) => serie.pointType !== PointType.NONE); const seriesForPointType = this.series.visibleSeries.filter((serie: ScatterData) => serie.pointType !== PointType.NONE);
if(seriesForPointType.length === 0) { if(seriesForPointType.length === 0) {
return undefined; // to avoid ts error return undefined; // to avoid ts error
} }
return this.concatSeriesDatapoints(seriesForPointType); return this.concatSeriesDatapoints(seriesForPointType);
} }
private concatSeriesDatapoints(series: ScatterData[]): number[][] { private concatSeriesDatapoints(series: ScatterData[]): DelaunayDataRow[] {
// return type row: [ 0:x, 1:y, 2?:custom value, last:serieIdx ]
const datapointsList = _.map(series, serie => { const datapointsList = _.map(series, serie => {
const datapointsWithOptions = _.map(serie.datapoints, row => _.concat(row, serie.idx)); const datapointsWithOptions = _.map(serie.datapoints, (row, rowIdx) => [row[0], row[1], row[2] || null, rowIdx, serie.target]);
return datapointsWithOptions; return datapointsWithOptions;
}); });
return _.union(...datapointsList); return _.union(...datapointsList);

6
src/models/scatter_series.ts

@ -1,6 +1,7 @@
import { CoreSeries } from '@chartwerk/core'; import { CoreSeries } from '@chartwerk/core';
import { ScatterData, PointType, LineType } from '../types'; import { ScatterData, PointType, LineType } from '../types';
import * as _ from 'lodash';
const DEFAULT_POINT_SIZE = 4; const DEFAULT_POINT_SIZE = 4;
@ -20,4 +21,9 @@ export class ScatterSeries extends CoreSeries<ScatterData> {
protected get defaults(): ScatterData { protected get defaults(): ScatterData {
return { ...this._coreDefaults, ...SCATTER_DATA_DEFAULTS }; return { ...this._coreDefaults, ...SCATTER_DATA_DEFAULTS };
} }
// move to parent
public getSerieByTarget(target: string): ScatterData | undefined {
return _.find(this.visibleSeries, serie => serie.target === target);
}
} }

24
src/types.ts

@ -26,4 +26,26 @@ export enum LineType {
DASHED = 'dashed' DASHED = 'dashed'
} }
export type ColorFormatter = (datapoint: number[], pointIdx) => string; export type ColorFormatter = (datapointsRow: any[], pointIdx) => string;
export type MouseMoveEvent = {
bbox: {
clientX: number,
clientY: number,
x: number,
y: number,
chartWidth: number,
chartHeight: number,
},
data: {
xval: number,
yval: number,
highlighted?: HighlightedData,
}
}
export type HighlightedData = {
xValue: number, yValue: number, customValue: number,
pointIdx: number, totalPointIdx: number,
serieInfo: { target: string, alias?: string, class?: string, idx?: number }
}

Loading…
Cancel
Save