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
pull/2/head
Alexander Velikiy 3 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 { 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 * as d3 from 'd3';
@ -14,6 +14,7 @@ const DEFAULT_LINE_DASHED_AMOUNT = 4;
export class ChartwerkScatterPod extends ChartwerkPod<ScatterData, ScatterOptions> {
metricContainer: any;
_delaunayDiagram: DelaunayDiagram;
series: ScatterSeries;
constructor(el: HTMLElement, _series: ScatterData[] = [], _options: ScatterOptions = {}) {
super(el, _series, _options);
@ -27,7 +28,7 @@ export class ChartwerkScatterPod extends ChartwerkPod<ScatterData, ScatterOption
}
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.renderPoints();
@ -115,41 +116,35 @@ export class ChartwerkScatterPod extends ChartwerkPod<ScatterData, ScatterOption
if(!this._delaunayDiagram.data) {
return;
}
this.metricContainer.selectAll(null)
.data(this._delaunayDiagram.data)
.enter()
.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('r', (d: number[]) => this.getSerieByIdx(_.last(d))?.pointSize)
.style('fill', (d: number[], i: number) => this.getSeriesColorFromDataRow(d, i))
.attr('r', (d: DelaunayDataRow) => this.series.getSerieByTarget(d[4])?.pointSize)
.style('fill', (d: DelaunayDataRow, i: number) => this.getSeriesColorFromDataRow(d, i))
.style('pointer-events', 'none')
.attr('cx', (d: any[]) => this.state.xScale(d[0]))
.attr('cy', (d: any[]) => this.getYScale(this.getSerieByIdx(_.last(d))?.yOrientation)(d[1]));
.attr('cx', (d: DelaunayDataRow) => this.state.xScale(d[0]))
.attr('cy', (d: DelaunayDataRow) => this.getYScale(this.series.getSerieByTarget(d[4])?.yOrientation)(d[1]));
this.metricContainer.selectAll(null)
.data(this._delaunayDiagram.data)
.enter()
.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('r', (d: number[]) => this.getSerieByIdx(_.last(d))?.pointSize)
.style('fill', (d: number[]) => this.getSerieByIdx(_.last(d))?.color)
.attr('r', (d: DelaunayDataRow) => this.series.getSerieByTarget(d[4])?.pointSize)
.style('fill', (d: DelaunayDataRow) => this.series.getSerieByTarget(d[4])?.color)
.style('pointer-events', 'none')
.attr('x', (d: number[]) => this.state.xScale(d[0]) - this.getSerieByIdx(_.last(d))?.pointSize / 2)
.attr('y', (d: number[]) => this.getYScale(this.getSerieByIdx(_.last(d)).yOrientation)(d[1]) - this.getSerieByIdx(_.last(d))?.pointSize / 2)
.attr('width', (d: number[]) => this.getSerieByIdx(_.last(d))?.pointSize)
.attr('height', (d: number[]) => this.getSerieByIdx(_.last(d))?.pointSize);
}
getSerieByIdx(idx: number): ScatterData | undefined {
return _.find(this.series.visibleSeries, serie => serie.idx === idx);
.attr('x', (d: DelaunayDataRow) => this.state.xScale(d[0]) - this.series.getSerieByTarget(d[4])?.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: DelaunayDataRow) => this.series.getSerieByTarget(d[4])?.pointSize)
.attr('height', (d: DelaunayDataRow) => this.series.getSerieByTarget(d[4])?.pointSize);
}
getSeriesColorFromDataRow(values: number[], rowIdx: number): string {
const seriesIdx = _.last(values);
const serie = this.getSerieByIdx(seriesIdx);
getSeriesColorFromDataRow(values: DelaunayDataRow, rowIdx: number): string {
const serie = this.series.getSerieByTarget(values[4]);
if(serie?.colorFormatter) {
return serie.colorFormatter(values, rowIdx);
}
@ -170,20 +165,18 @@ export class ChartwerkScatterPod extends ChartwerkPod<ScatterData, ScatterOption
highlight(pointIdx: number): void {
this.unhighlight();
const datapoint = this._delaunayDiagram.getDataRowByIndex(pointIdx);
if(datapoint === undefined || datapoint === null) {
const row = this._delaunayDiagram.getDataRowByIndex(pointIdx);
if(row === undefined || row === null) {
return;
}
const serieIdx = _.last(datapoint);
const serie = this.getSerieByIdx(serieIdx);
const serie = this.series.getSerieByTarget(row[4]);
const size = this.getCrosshairCircleBackgroundSize(serie.pointSize, serie.pointType);
this.crosshair.selectAll(`.crosshair-point-${serieIdx}`)
.attr('cx', this.state.xScale(datapoint[0]))
.attr('cy', this.getYScale(serie.yOrientation)(datapoint[1]))
.attr('x', this.state.xScale(datapoint[0]) - size / 2)
.attr('y', this.getYScale(serie.yOrientation)(datapoint[1]) - size / 2)
.attr('fill', this.getSeriesColorFromDataRow(datapoint, pointIdx))
this.crosshair.selectAll(`.crosshair-point-${serie.idx}`)
.attr('cx', this.state.xScale(row[0]))
.attr('cy', this.getYScale(serie.yOrientation)(row[1]))
.attr('x', this.state.xScale(row[0]) - size / 2)
.attr('y', this.getYScale(serie.yOrientation)(row[1]) - size / 2)
.attr('fill', this.getSeriesColorFromDataRow(row, pointIdx))
.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) {
return null;
}
const pointIndex = this._delaunayDiagram.findPointIndex(eventX, eventY);
if(pointIndex === undefined) {
this.unhighlight();
return null;
}
this.highlight(pointIndex);
const row = this._delaunayDiagram.data[pointIndex];
const serie = this.series.getSerieByTarget(row[4]);
return {
values: this._delaunayDiagram.data[pointIndex],
pointIdx: pointIndex,
xValue: row[0],
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
const highlighted = this.findAndHighlightDatapoints(eventX, eventY);
// TODO: group fields
this.options.callbackMouseMove({
x: d3.event.clientX,
y: d3.event.clientY,
xval: this.state.xScale.invert(eventX),
yval: this.state.xScale.invert(eventY),
highlighted,
chartX: eventX,
chartWidth: this.width
bbox: {
clientX: d3.event.clientX,
clientY: d3.event.clientY,
x: eventX,
y: eventY,
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 { ScatterSeries } from './scatter_series';
import { Delaunay } from 'd3-delaunay';
import * as _ from 'lodash';
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 {
private _delaunayData: number[][]; // [ 0:y, 1:x, ..., last:serieIdx ][]
private _delaunayData: DelaunayDataRow[];
private _delaunayDiagram: any;
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
) {
this._delaunayData = this.getDatapointsForDelaunay();
this.setDelaunayDiagram(xScale, yScale);
}
public get data(): number[][] | undefined {
public get data(): DelaunayDataRow[] | undefined {
if(!this._delaunayData || this._delaunayData.length === 0) {
return undefined;
}
@ -33,8 +39,8 @@ export class DelaunayDiagram {
console.time('delaunay-init');
this._delaunayDiagram = Delaunay.from(
this._delaunayData,
(d: number[]) => xScale(d[0]),
(d: number[]) => yScale(this.series[_.last(d)].yOrientation)(d[1]),
(d: DelaunayDataRow) => xScale(d[0]),
(d: DelaunayDataRow) => yScale(this.series.getSerieByTarget(d[4])?.yOrientation)(d[1]),
);
console.timeEnd('delaunay-init');
}
@ -51,28 +57,27 @@ export class DelaunayDiagram {
return pointIndex;
}
public getDataRowByIndex(index: number): number[] | undefined {
public getDataRowByIndex(index: number): DelaunayDataRow | undefined {
if(!this.data) {
return undefined;
}
return this.data[index];
}
private getDatapointsForDelaunay(): number[][] | undefined {
private getDatapointsForDelaunay(): DelaunayDataRow[] | undefined {
// here we union all datapoints with point render type(circle or rectangle)
// it means that circles and rectangles will be highlighted(not lines)
// 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) {
return undefined; // to avoid ts error
}
return this.concatSeriesDatapoints(seriesForPointType);
}
private concatSeriesDatapoints(series: ScatterData[]): number[][] {
// return type row: [ 0:x, 1:y, 2?:custom value, last:serieIdx ]
private concatSeriesDatapoints(series: ScatterData[]): DelaunayDataRow[] {
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 _.union(...datapointsList);

6
src/models/scatter_series.ts

@ -1,6 +1,7 @@
import { CoreSeries } from '@chartwerk/core';
import { ScatterData, PointType, LineType } from '../types';
import * as _ from 'lodash';
const DEFAULT_POINT_SIZE = 4;
@ -20,4 +21,9 @@ export class ScatterSeries extends CoreSeries<ScatterData> {
protected get defaults(): ScatterData {
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'
}
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