vargburz
3 years ago
16 changed files with 5466 additions and 2 deletions
@ -0,0 +1,35 @@ |
|||||||
|
const path = require('path'); |
||||||
|
|
||||||
|
|
||||||
|
function resolve(dir) { |
||||||
|
return path.join(__dirname, '..', dir) |
||||||
|
} |
||||||
|
|
||||||
|
module.exports = { |
||||||
|
context: resolve('src'), |
||||||
|
entry: './index.ts', |
||||||
|
plugins: [], |
||||||
|
module: { |
||||||
|
rules: [ |
||||||
|
{ |
||||||
|
test: /\.ts$/, |
||||||
|
use: 'ts-loader', |
||||||
|
exclude: /node_modules/ |
||||||
|
}, |
||||||
|
{ |
||||||
|
test: /\.css$/, |
||||||
|
use: ['style-loader', 'css-loader'], |
||||||
|
exclude: /node_modules/ |
||||||
|
} |
||||||
|
], |
||||||
|
}, |
||||||
|
resolve: { |
||||||
|
extensions: ['.ts', '.js'], |
||||||
|
}, |
||||||
|
output: { |
||||||
|
filename: 'index.js', |
||||||
|
path: resolve('dist'), |
||||||
|
libraryTarget: 'umd', |
||||||
|
umdNamedDefine: true |
||||||
|
} |
||||||
|
}; |
@ -0,0 +1,8 @@ |
|||||||
|
const baseWebpackConfig = require('./webpack.base.conf'); |
||||||
|
|
||||||
|
var conf = baseWebpackConfig; |
||||||
|
conf.devtool = 'inline-source-map'; |
||||||
|
conf.watch = true; |
||||||
|
conf.mode = 'development'; |
||||||
|
|
||||||
|
module.exports = conf; |
@ -0,0 +1,6 @@ |
|||||||
|
const baseWebpackConfig = require('./webpack.base.conf'); |
||||||
|
|
||||||
|
var conf = baseWebpackConfig; |
||||||
|
conf.mode = 'production'; |
||||||
|
|
||||||
|
module.exports = baseWebpackConfig; |
@ -0,0 +1,86 @@ |
|||||||
|
<!DOCTYPE html> |
||||||
|
<html> |
||||||
|
<head> |
||||||
|
<meta content="text/html;charset=utf-8" http-equiv="Content-Type"> |
||||||
|
<meta content="utf-8" http-equiv="encoding"> |
||||||
|
|
||||||
|
<script src="./dist/index.js" type="text/javascript"></script> |
||||||
|
</head> |
||||||
|
<body> |
||||||
|
<div id="chart" style="width: 500px; height: 500px;"></div> |
||||||
|
|
||||||
|
<script type="text/javascript"> |
||||||
|
var pod = new ChartwerkScatterPod( |
||||||
|
document.getElementById('chart'), |
||||||
|
[ |
||||||
|
{ |
||||||
|
target: 'test1', |
||||||
|
datapoints: [ |
||||||
|
[100, -50, 0], |
||||||
|
[200, 150, 0], |
||||||
|
[100, 160, 1], |
||||||
|
[150, 170, 1], |
||||||
|
[150, 180, 0], |
||||||
|
[150, 250, 1] |
||||||
|
], |
||||||
|
color: 'red', |
||||||
|
lineType: 'dashed', |
||||||
|
pointType: 'circle' |
||||||
|
}, |
||||||
|
{ |
||||||
|
target: 'test2', |
||||||
|
datapoints: [ |
||||||
|
[200, 50, 1], |
||||||
|
[175, 60, 0], |
||||||
|
[150, 70, 1] |
||||||
|
], |
||||||
|
color: 'purple', |
||||||
|
pointType: 'rectangle', |
||||||
|
pointSize: 5, |
||||||
|
yOrientation: 'right', |
||||||
|
} |
||||||
|
], |
||||||
|
{ |
||||||
|
axis: { |
||||||
|
x: { |
||||||
|
format: 'numeric', |
||||||
|
range: [-100, 300] |
||||||
|
}, |
||||||
|
y: { |
||||||
|
invert: true, |
||||||
|
range: [-100, 250] |
||||||
|
}, |
||||||
|
y1: { |
||||||
|
isActive: true, |
||||||
|
range: [0, 250] |
||||||
|
} |
||||||
|
}, |
||||||
|
zoomEvents: { |
||||||
|
mouse: { |
||||||
|
pan: { isActive: false, orientation: 'both', keyEvent: 'main' }, |
||||||
|
zoom: { isActive: true, keyEvent: 'shift' }, |
||||||
|
}, |
||||||
|
scroll: { |
||||||
|
pan: { isActive: false }, |
||||||
|
zoom: { isActive: true, keyEvent: 'main' } |
||||||
|
} |
||||||
|
}, |
||||||
|
crosshair: { |
||||||
|
orientation: 'both', |
||||||
|
color: 'gray' |
||||||
|
}, |
||||||
|
labelFormat: { |
||||||
|
yAxis: 'y', |
||||||
|
xAxis: 'x' |
||||||
|
}, |
||||||
|
eventsCallbacks: { |
||||||
|
zoomOut: () => { pod.render() } |
||||||
|
}, |
||||||
|
margin: { top: 30, right: 30, bottom: 40, left: 30 }, |
||||||
|
circleView: true, |
||||||
|
} |
||||||
|
); |
||||||
|
pod.render(); |
||||||
|
</script> |
||||||
|
</body> |
||||||
|
</html> |
@ -0,0 +1,15 @@ |
|||||||
|
import { ScatterData } from './types'; |
||||||
|
import * as d3 from 'd3'; |
||||||
|
export declare class DelaunayDiagram { |
||||||
|
protected series: ScatterData[]; |
||||||
|
private _delaunayData; |
||||||
|
private _delaunayDiagram; |
||||||
|
constructor(series: ScatterData[], xScale: d3.ScaleLinear<number, number>, yScale: (string: any) => d3.ScaleLinear<number, number>); |
||||||
|
get data(): number[][] | undefined; |
||||||
|
setDelaunayDiagram(xScale: d3.ScaleLinear<number, number>, yScale: (string: any) => d3.ScaleLinear<number, number>): void; |
||||||
|
findPointIndex(eventX: number, eventY: number): number | undefined; |
||||||
|
getDataRowByIndex(index: number): number[] | undefined; |
||||||
|
private getDatapointsForDelaunay; |
||||||
|
private concatSeriesDatapoints; |
||||||
|
private getSerieIdxByTarget; |
||||||
|
} |
@ -0,0 +1,88 @@ |
|||||||
|
import { ChartwerkPod, TickOrientation, TimeFormat, yAxisOrientation } from '@chartwerk/core'; |
||||||
|
import { ScatterData, ScatterOptions, PointType, LineType } from './types'; |
||||||
|
import { DelaunayDiagram } from './delaunay'; |
||||||
|
import * as d3 from 'd3'; |
||||||
|
export declare class ChartwerkScatterPod extends ChartwerkPod<ScatterData, ScatterOptions> { |
||||||
|
_metricsContainer: any; |
||||||
|
_delaunayDiagram: DelaunayDiagram; |
||||||
|
constructor(el: HTMLElement, _series?: ScatterData[], _options?: ScatterOptions); |
||||||
|
renderMetrics(): void; |
||||||
|
renderMetricContainer(): void; |
||||||
|
protected updateCrosshair(): void; |
||||||
|
appendCrosshairPoints(): void; |
||||||
|
protected appendCrosshairPoint(serieIdx: number): void; |
||||||
|
protected renderLines(): void; |
||||||
|
renderLine(datapoints: number[][], lineType: LineType, color: string, orientation: yAxisOrientation): void; |
||||||
|
protected renderPoints(): void; |
||||||
|
onPanningEnd(): void; |
||||||
|
unhighlight(): void; |
||||||
|
highlight(pointIdx: number): void; |
||||||
|
protected getCrosshairCircleBackgroundSize(serieIdx: number): number; |
||||||
|
renderSharedCrosshair(values: { |
||||||
|
x?: number; |
||||||
|
y?: number; |
||||||
|
}): void; |
||||||
|
moveCrosshairLine(xPosition: number, yPosition: number): void; |
||||||
|
findAndHighlightDatapoints(eventX: number, eventY: number): { |
||||||
|
values: any[]; |
||||||
|
pointIdx: number; |
||||||
|
} | null; |
||||||
|
protected getYScale(orientation: yAxisOrientation): d3.ScaleLinear<number, number>; |
||||||
|
hideSharedCrosshair(): void; |
||||||
|
onMouseMove(): void; |
||||||
|
onMouseOver(): void; |
||||||
|
onMouseOut(): void; |
||||||
|
} |
||||||
|
export declare const VueChartwerkScatterPodObject: { |
||||||
|
render(createElement: any): any; |
||||||
|
mixins: { |
||||||
|
props: { |
||||||
|
id: { |
||||||
|
type: StringConstructor; |
||||||
|
required: boolean; |
||||||
|
}; |
||||||
|
series: { |
||||||
|
type: ArrayConstructor; |
||||||
|
required: boolean; |
||||||
|
default: () => any[]; |
||||||
|
}; |
||||||
|
options: { |
||||||
|
type: ObjectConstructor; |
||||||
|
required: boolean; |
||||||
|
default: () => {}; |
||||||
|
}; |
||||||
|
}; |
||||||
|
watch: { |
||||||
|
id(): void; |
||||||
|
series(): void; |
||||||
|
options(): void; |
||||||
|
}; |
||||||
|
mounted(): void; |
||||||
|
destroyed(): void; |
||||||
|
methods: { |
||||||
|
render(): void; |
||||||
|
renderSharedCrosshair(values: { |
||||||
|
x?: number; |
||||||
|
y?: number; |
||||||
|
}): void; |
||||||
|
hideSharedCrosshair(): void; |
||||||
|
onPanningRescale(event: any): void; |
||||||
|
renderChart(): void; |
||||||
|
appendEvents(): void; |
||||||
|
zoomIn(range: any): void; |
||||||
|
zoomOut(centers: any): void; |
||||||
|
mouseMove(evt: any): void; |
||||||
|
mouseOut(): void; |
||||||
|
onLegendClick(idx: any): void; |
||||||
|
panningEnd(range: any): void; |
||||||
|
panning(range: any): void; |
||||||
|
contextMenu(evt: any): void; |
||||||
|
sharedCrosshairMove(event: any): void; |
||||||
|
renderEnd(): void; |
||||||
|
}; |
||||||
|
}[]; |
||||||
|
methods: { |
||||||
|
render(): void; |
||||||
|
}; |
||||||
|
}; |
||||||
|
export { ScatterData, ScatterOptions, TickOrientation, TimeFormat, PointType, LineType }; |
File diff suppressed because one or more lines are too long
@ -0,0 +1,26 @@ |
|||||||
|
import { TimeSerie, Options } from '@chartwerk/core'; |
||||||
|
declare type ScatterDataParams = { |
||||||
|
pointType: PointType; |
||||||
|
lineType: LineType; |
||||||
|
pointSize: number; |
||||||
|
colorFormatter?: ColorFormatter; |
||||||
|
}; |
||||||
|
declare type ScatterOptionsParams = { |
||||||
|
voronoiRadius: number; |
||||||
|
circleView: boolean; |
||||||
|
renderGrid: boolean; |
||||||
|
}; |
||||||
|
export declare type ScatterData = TimeSerie & Partial<ScatterDataParams>; |
||||||
|
export declare type ScatterOptions = Options & Partial<ScatterOptionsParams>; |
||||||
|
export declare enum PointType { |
||||||
|
NONE = "none", |
||||||
|
CIRCLE = "circle", |
||||||
|
RECTANGLE = "rectangle" |
||||||
|
} |
||||||
|
export declare enum LineType { |
||||||
|
NONE = "none", |
||||||
|
SOLID = "solid", |
||||||
|
DASHED = "dashed" |
||||||
|
} |
||||||
|
export declare type ColorFormatter = (datapoint: number[]) => string; |
||||||
|
export {}; |
@ -0,0 +1,30 @@ |
|||||||
|
{ |
||||||
|
"name": "@chartwerk/scatter-pod", |
||||||
|
"version": "0.2.4", |
||||||
|
"description": "Chartwerk scatter pod", |
||||||
|
"main": "dist/index.js", |
||||||
|
"scripts": { |
||||||
|
"build": "webpack --config build/webpack.prod.conf.js", |
||||||
|
"dev": "webpack --config build/webpack.dev.conf.js", |
||||||
|
"test": "echo \"Error: no test specified\" && exit 1" |
||||||
|
}, |
||||||
|
"repository": {}, |
||||||
|
"author": "CorpGlory", |
||||||
|
"license": "Apache-2.0", |
||||||
|
"dependencies": { |
||||||
|
"@chartwerk/core": "github:chartwerk/core#532eddbc8ad938091b1d9ec1693cec5eddfdbfc2" |
||||||
|
}, |
||||||
|
"devDependencies": { |
||||||
|
"@types/d3": "^5.7.2", |
||||||
|
"@types/lodash": "^4.14.149", |
||||||
|
"css-loader": "^3.4.2", |
||||||
|
"d3": "^5.15.0", |
||||||
|
"d3-delaunay": "^6.0.2", |
||||||
|
"lodash": "^4.17.15", |
||||||
|
"style-loader": "^1.1.3", |
||||||
|
"ts-loader": "^6.2.1", |
||||||
|
"typescript": "^3.8.3", |
||||||
|
"webpack": "^4.42.0", |
||||||
|
"webpack-cli": "^3.3.11" |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,89 @@ |
|||||||
|
import { ScatterData, PointType } from './types'; |
||||||
|
|
||||||
|
import { Delaunay } from 'd3-delaunay'; |
||||||
|
|
||||||
|
import * as _ from 'lodash'; |
||||||
|
import * as d3 from 'd3'; // only types
|
||||||
|
|
||||||
|
|
||||||
|
export class DelaunayDiagram { |
||||||
|
private _delaunayData: number[][]; // [ 0:y, 1:x, ..., last:serieIdx ][]
|
||||||
|
private _delaunayDiagram: any; |
||||||
|
|
||||||
|
constructor( |
||||||
|
protected series: ScatterData[], |
||||||
|
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 { |
||||||
|
if(!this._delaunayData || this._delaunayData.length === 0) { |
||||||
|
return undefined; |
||||||
|
} |
||||||
|
return this._delaunayData; |
||||||
|
} |
||||||
|
|
||||||
|
public setDelaunayDiagram(xScale: d3.ScaleLinear<number, number>, yScale: (string) => d3.ScaleLinear<number, number>) { |
||||||
|
if(!this._delaunayData) { |
||||||
|
console.warn('No data for delaunay initialization'); |
||||||
|
return; |
||||||
|
} |
||||||
|
console.time('delaunay-init'); |
||||||
|
this._delaunayDiagram = Delaunay.from( |
||||||
|
this._delaunayData, |
||||||
|
(d: number[]) => xScale(d[1]), |
||||||
|
(d: number[]) => yScale(this.series[_.last(d)].yOrientation)(d[0]), |
||||||
|
); |
||||||
|
console.timeEnd('delaunay-init'); |
||||||
|
} |
||||||
|
|
||||||
|
public findPointIndex(eventX: number, eventY: number): number | undefined { |
||||||
|
if(!this._delaunayDiagram) { |
||||||
|
return undefined; |
||||||
|
} |
||||||
|
let pointIndex = this._delaunayDiagram.find(eventX, eventY); |
||||||
|
if(pointIndex === -1) { |
||||||
|
return undefined; |
||||||
|
} |
||||||
|
// TODO: add search radius via https://github.com/d3/d3-delaunay/issues/45
|
||||||
|
return pointIndex; |
||||||
|
} |
||||||
|
|
||||||
|
public getDataRowByIndex(index: number): number[] | undefined { |
||||||
|
if(!this.data) { |
||||||
|
return undefined; |
||||||
|
} |
||||||
|
return this.data[index]; |
||||||
|
} |
||||||
|
|
||||||
|
private getDatapointsForDelaunay(): number[][] | 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); |
||||||
|
if(seriesForPointType.length === 0) { |
||||||
|
return undefined; // to avoid ts error
|
||||||
|
} |
||||||
|
return this.concatSeriesDatapoints(seriesForPointType); |
||||||
|
} |
||||||
|
|
||||||
|
private concatSeriesDatapoints(series: ScatterData[]): number[][] { |
||||||
|
// return type row: [ 0:y, 1:x, 2?:custom value, last:serieIdx ]
|
||||||
|
const datapointsList = _.map(series, serie => { |
||||||
|
const serieIdx = this.getSerieIdxByTarget(serie.target); |
||||||
|
const datapointsWithOptions = _.map(serie.datapoints, row => _.concat(row, serieIdx)); |
||||||
|
return datapointsWithOptions; |
||||||
|
}); |
||||||
|
return _.union(...datapointsList); |
||||||
|
} |
||||||
|
|
||||||
|
private getSerieIdxByTarget(target: string): number { |
||||||
|
const idx = _.findIndex(this.series, serie => serie.target === target); |
||||||
|
if(idx === -1) { |
||||||
|
throw new Error(`Can't find serie with target: ${target}`); |
||||||
|
} |
||||||
|
return idx; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,359 @@ |
|||||||
|
import { ChartwerkPod, VueChartwerkPodMixin, TickOrientation, TimeFormat, yAxisOrientation, CrosshairOrientation, PanOrientation } from '@chartwerk/core'; |
||||||
|
import { ScatterData, ScatterOptions, PointType, LineType, ColorFormatter } from './types'; |
||||||
|
|
||||||
|
import { DelaunayDiagram } from './delaunay'; |
||||||
|
|
||||||
|
import * as d3 from 'd3'; |
||||||
|
import * as _ from 'lodash'; |
||||||
|
|
||||||
|
// TODO: use pod state with defaults
|
||||||
|
const DEFAULT_POINT_SIZE = 4; |
||||||
|
const POINT_HIGHLIGHT_DIAMETER = 4; |
||||||
|
const CROSSHAIR_BACKGROUND_OPACITY = 0.3; |
||||||
|
const DEFAULT_POINT_TYPE = PointType.CIRCLE; |
||||||
|
const DEFAULT_LINE_TYPE = LineType.NONE; |
||||||
|
const DEFAULT_LINE_DASHED_AMOUNT = 4; |
||||||
|
|
||||||
|
export class ChartwerkScatterPod extends ChartwerkPod<ScatterData, ScatterOptions> { |
||||||
|
_metricsContainer: any; |
||||||
|
_delaunayDiagram: DelaunayDiagram; |
||||||
|
|
||||||
|
constructor(el: HTMLElement, _series: ScatterData[] = [], _options: ScatterOptions = {}) { |
||||||
|
super(d3, el, _series, _options); |
||||||
|
} |
||||||
|
|
||||||
|
renderMetrics(): void { |
||||||
|
if(this.series.length === 0) { |
||||||
|
this.renderNoDataPointsMessage(); |
||||||
|
return; |
||||||
|
} |
||||||
|
this.updateCrosshair();
|
||||||
|
this.renderMetricContainer(); |
||||||
|
|
||||||
|
this._delaunayDiagram = new DelaunayDiagram(this.series, this.xScale, this.getYScale.bind(this)); |
||||||
|
|
||||||
|
this.renderLines(); |
||||||
|
this.renderPoints(); |
||||||
|
} |
||||||
|
|
||||||
|
renderMetricContainer(): void { |
||||||
|
// container for clip path
|
||||||
|
const clipContatiner = this.chartContainer |
||||||
|
.append('g') |
||||||
|
.attr('clip-path', `url(#${this.rectClipId})`) |
||||||
|
.attr('class', 'metrics-container'); |
||||||
|
// container for panning
|
||||||
|
this._metricsContainer = clipContatiner |
||||||
|
.append('g') |
||||||
|
.attr('class', ' metrics-rect'); |
||||||
|
} |
||||||
|
|
||||||
|
protected updateCrosshair(): void { |
||||||
|
// TODO: Crosshair class, which can be used as Pod
|
||||||
|
this.appendCrosshairPoints(); |
||||||
|
} |
||||||
|
|
||||||
|
appendCrosshairPoints(): void { |
||||||
|
this.series.forEach((serie: ScatterData, serieIdx: number) => { |
||||||
|
this.appendCrosshairPoint(serieIdx); |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
protected appendCrosshairPoint(serieIdx: number): void { |
||||||
|
// TODO: add Crosshair type options
|
||||||
|
const pointType = this.series[serieIdx].pointType || DEFAULT_POINT_TYPE; |
||||||
|
switch(pointType) { |
||||||
|
case PointType.NONE: |
||||||
|
return; |
||||||
|
case PointType.CIRCLE: |
||||||
|
this.crosshair.append('circle') |
||||||
|
.attr('class', `crosshair-point crosshair-point-${serieIdx} crosshair-background`) |
||||||
|
.attr('r', this.getCrosshairCircleBackgroundSize(serieIdx)) |
||||||
|
.attr('clip-path', `url(#${this.rectClipId})`) |
||||||
|
.style('opacity', CROSSHAIR_BACKGROUND_OPACITY) |
||||||
|
.style('pointer-events', 'none') |
||||||
|
.style('display', 'none'); |
||||||
|
return; |
||||||
|
case PointType.RECTANGLE: |
||||||
|
this.crosshair.append('rect') |
||||||
|
.attr('class', `crosshair-point crosshair-point-${serieIdx} crosshair-background`) |
||||||
|
.attr('width', this.getCrosshairCircleBackgroundSize(serieIdx)) |
||||||
|
.attr('height', this.getCrosshairCircleBackgroundSize(serieIdx)) |
||||||
|
.attr('clip-path', `url(#${this.rectClipId})`) |
||||||
|
.style('opacity', CROSSHAIR_BACKGROUND_OPACITY) |
||||||
|
.style('pointer-events', 'none') |
||||||
|
.style('display', 'none'); |
||||||
|
return; |
||||||
|
default: |
||||||
|
throw new Error(`Unknown render point type: ${pointType}`); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
protected renderLines(): void { |
||||||
|
this.series.forEach((serie, serieIdx) => { |
||||||
|
if(serie.visible === false) { |
||||||
|
return; |
||||||
|
} |
||||||
|
const lineType = serie.lineType || DEFAULT_LINE_TYPE; |
||||||
|
this.renderLine(serie.datapoints, lineType, this.getSerieColor(serieIdx), serie.yOrientation); |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
renderLine(datapoints: number[][], lineType: LineType, color: string, orientation: yAxisOrientation): void { |
||||||
|
if(lineType === LineType.NONE) { |
||||||
|
return; |
||||||
|
} |
||||||
|
let strokeDasharray; |
||||||
|
// TODO: move to option
|
||||||
|
if(lineType === LineType.DASHED) { |
||||||
|
strokeDasharray = DEFAULT_LINE_DASHED_AMOUNT; |
||||||
|
} |
||||||
|
const lineGenerator = this.d3.line() |
||||||
|
.x((d: [number, number]) => this.xScale(d[1])) |
||||||
|
.y((d: [number, number]) => this.getYScale(orientation)(d[0])); |
||||||
|
|
||||||
|
this._metricsContainer |
||||||
|
.append('path') |
||||||
|
.datum(datapoints) |
||||||
|
.attr('class', 'metric-path') |
||||||
|
.attr('fill', 'none') |
||||||
|
.style('pointer-events', 'none') |
||||||
|
.attr('stroke', color) |
||||||
|
.attr('stroke-width', 1) |
||||||
|
.attr('stroke-opacity', 0.7) |
||||||
|
.attr('stroke-dasharray', strokeDasharray) |
||||||
|
.attr('d', lineGenerator); |
||||||
|
} |
||||||
|
|
||||||
|
protected renderPoints(): void { |
||||||
|
if(!this._delaunayDiagram.data) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
this._metricsContainer.selectAll(null) |
||||||
|
.data(this._delaunayDiagram.data) |
||||||
|
.enter() |
||||||
|
.append('circle') |
||||||
|
.filter((d: number[]) => this.series[_.last(d)].pointType !== PointType.RECTANGLE) |
||||||
|
.attr('class', (d, i: number) => `metric-element metric-circle point-${i}`) |
||||||
|
.attr('r', (d: number[]) => this.series[_.last(d)].pointSize || DEFAULT_POINT_SIZE) |
||||||
|
.style('fill', (d: number[]) => this.getSerieColor(_.last(d))) |
||||||
|
.style('pointer-events', 'none') |
||||||
|
.attr('cx', (d: any[]) => this.xScale(d[1])) |
||||||
|
.attr('cy', (d: any[]) => this.getYScale(this.series[_.last(d)].yOrientation)(d[0])); |
||||||
|
|
||||||
|
this._metricsContainer.selectAll(null) |
||||||
|
.data(this._delaunayDiagram.data) |
||||||
|
.enter() |
||||||
|
.append('rect') |
||||||
|
.filter((d: number[]) => this.series[_.last(d)].pointType === PointType.RECTANGLE) |
||||||
|
.attr('class', (d, i: number) => `metric-element metric-circle point-${i}`) |
||||||
|
.attr('r', (d: number[]) => this.series[_.last(d)].pointSize || DEFAULT_POINT_SIZE) |
||||||
|
.style('fill', (d: number[]) => this.getSerieColor(_.last(d))) |
||||||
|
.style('pointer-events', 'none') |
||||||
|
.attr('x', (d: number[]) => this.xScale(d[1]) - (this.series[_.last(d)].pointSize || DEFAULT_POINT_SIZE) / 2) |
||||||
|
.attr('y', (d: number[]) => this.getYScale(this.series[_.last(d)].yOrientation)(d[0]) - (this.series[_.last(d)].pointSize || DEFAULT_POINT_SIZE) / 2) |
||||||
|
.attr('width', (d: number[]) => this.series[_.last(d)].pointSize || DEFAULT_POINT_SIZE) |
||||||
|
.attr('height', (d: number[]) => this.series[_.last(d)].pointSize || DEFAULT_POINT_SIZE); |
||||||
|
} |
||||||
|
|
||||||
|
onPanningEnd(): void { |
||||||
|
this.isPanning = false; |
||||||
|
this.onMouseOut(); |
||||||
|
this._delaunayDiagram.setDelaunayDiagram(this.xScale, this.getYScale.bind(this)); |
||||||
|
if(this.options.eventsCallbacks !== undefined && this.options.eventsCallbacks.panningEnd !== undefined) { |
||||||
|
this.options.eventsCallbacks.panningEnd([this.state.xValueRange, this.state.yValueRange, this.state.y1ValueRange]); |
||||||
|
} else { |
||||||
|
console.log('on panning end, but there is no callback'); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
unhighlight(): void { |
||||||
|
this.crosshair.selectAll('.crosshair-point').style('display', 'none'); |
||||||
|
} |
||||||
|
|
||||||
|
highlight(pointIdx: number): void { |
||||||
|
this.unhighlight(); |
||||||
|
|
||||||
|
const datapoint = this._delaunayDiagram.getDataRowByIndex(pointIdx); |
||||||
|
if(datapoint === undefined || datapoint === null) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
const serieIdx = _.last(datapoint); |
||||||
|
const serieOrientation = this.series[serieIdx].yOrientation; |
||||||
|
const size = this.getCrosshairCircleBackgroundSize(serieIdx); |
||||||
|
const colorFormatter = this.series[serieIdx].colorFormatter; |
||||||
|
this.crosshair.selectAll(`.crosshair-point-${serieIdx}`) |
||||||
|
.attr('cx', this.xScale(datapoint[1])) |
||||||
|
.attr('cy', this.getYScale(serieOrientation)(datapoint[0])) |
||||||
|
.attr('x', this.xScale(datapoint[1]) - size / 2) |
||||||
|
.attr('y', this.getYScale(serieOrientation)(datapoint[0]) - size / 2) |
||||||
|
.attr('fill', colorFormatter !== undefined ? colorFormatter(datapoint) : this.series[serieIdx].color) |
||||||
|
.style('display', null); |
||||||
|
} |
||||||
|
|
||||||
|
protected getCrosshairCircleBackgroundSize(serieIdx: number): number { |
||||||
|
const seriePointSize = this.series[serieIdx].pointSize || DEFAULT_POINT_SIZE; |
||||||
|
const pointType = this.series[serieIdx].pointType || DEFAULT_POINT_TYPE; |
||||||
|
let highlightDiameter = POINT_HIGHLIGHT_DIAMETER; |
||||||
|
if(pointType === PointType.RECTANGLE) { |
||||||
|
highlightDiameter = highlightDiameter * 2; |
||||||
|
} |
||||||
|
return seriePointSize + highlightDiameter; |
||||||
|
} |
||||||
|
|
||||||
|
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); |
||||||
|
this.moveCrosshairLine(eventX, eventY); |
||||||
|
const datapoints = this.findAndHighlightDatapoints(values.x, values.y); |
||||||
|
|
||||||
|
if(this.options.eventsCallbacks === undefined || this.options.eventsCallbacks.sharedCrosshairMove === undefined) { |
||||||
|
console.log('Shared crosshair move, but there is no callback'); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
this.options.eventsCallbacks.sharedCrosshairMove({ |
||||||
|
datapoints, |
||||||
|
eventX, eventY |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
moveCrosshairLine(xPosition: number, yPosition: number): void { |
||||||
|
switch (this.options.crosshair.orientation) { |
||||||
|
case CrosshairOrientation.VERTICAL: |
||||||
|
this.crosshair.select('#crosshair-line-x') |
||||||
|
.attr('x1', xPosition) |
||||||
|
.attr('x2', xPosition); |
||||||
|
return; |
||||||
|
case CrosshairOrientation.HORIZONTAL: |
||||||
|
this.crosshair.select('#crosshair-line-y') |
||||||
|
.attr('y1', yPosition) |
||||||
|
.attr('y2', yPosition); |
||||||
|
return; |
||||||
|
case CrosshairOrientation.BOTH: |
||||||
|
this.crosshair.select('#crosshair-line-x') |
||||||
|
.attr('x1', xPosition) |
||||||
|
.attr('x2', xPosition); |
||||||
|
this.crosshair.select('#crosshair-line-y') |
||||||
|
.attr('y1', yPosition) |
||||||
|
.attr('y2', yPosition); |
||||||
|
return; |
||||||
|
default: |
||||||
|
throw new Error(`Unknown type of crosshair orientaion: ${this.options.crosshair.orientation}`); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
findAndHighlightDatapoints(eventX: number, eventY: number): { values: any[], pointIdx: number } | null { |
||||||
|
if(this.series === undefined || this.series.length === 0) { |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
const pointIndex = this._delaunayDiagram.findPointIndex(eventX, eventY); |
||||||
|
if(pointIndex === undefined) { |
||||||
|
this.unhighlight(); |
||||||
|
return null; |
||||||
|
} |
||||||
|
this.highlight(pointIndex); |
||||||
|
|
||||||
|
return { |
||||||
|
values: this._delaunayDiagram.data[pointIndex], |
||||||
|
pointIdx: pointIndex, |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
protected getYScale(orientation: yAxisOrientation): d3.ScaleLinear<number, number> { |
||||||
|
if(orientation === undefined || orientation === yAxisOrientation.BOTH) { |
||||||
|
return this.yScale; |
||||||
|
} |
||||||
|
switch(orientation) { |
||||||
|
case yAxisOrientation.LEFT: |
||||||
|
return this.yScale; |
||||||
|
case yAxisOrientation.RIGHT: |
||||||
|
return this.y1Scale; |
||||||
|
default: |
||||||
|
throw new Error(`Unknown type of y axis orientation: ${orientation}`)
|
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
public hideSharedCrosshair(): void { |
||||||
|
this.crosshair.style('display', 'none'); |
||||||
|
} |
||||||
|
|
||||||
|
onMouseMove(): void { |
||||||
|
const mousePosition = this.d3.mouse(this.chartContainer.node()); |
||||||
|
const eventX = mousePosition[0]; |
||||||
|
const eventY = mousePosition[1]; |
||||||
|
|
||||||
|
// TODO: seems isOutOfChart is deprecated (check clippath correctness)
|
||||||
|
if(this.isOutOfChart() === true || this.isPanning === true || this.isBrushing === true) { |
||||||
|
this.crosshair.style('display', 'none'); |
||||||
|
return; |
||||||
|
} else { |
||||||
|
this.crosshair.style('display', null); |
||||||
|
} |
||||||
|
|
||||||
|
this.moveCrosshairLine(eventX, eventY); |
||||||
|
|
||||||
|
// TOOD: it should be two different methods
|
||||||
|
const highlighted = this.findAndHighlightDatapoints(eventX, eventY); |
||||||
|
if(this.options.eventsCallbacks === undefined || this.options.eventsCallbacks.mouseMove === undefined) { |
||||||
|
console.log('Mouse move, but there is no callback'); |
||||||
|
return; |
||||||
|
} |
||||||
|
// TODO: group fields
|
||||||
|
this.options.eventsCallbacks.mouseMove({ |
||||||
|
x: this.d3.event.clientX, |
||||||
|
y: this.d3.event.clientY, |
||||||
|
xval: this.xScale.invert(eventX), |
||||||
|
yval: this.xScale.invert(eventY), |
||||||
|
highlighted, |
||||||
|
chartX: eventX, |
||||||
|
chartWidth: this.width |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
onMouseOver(): void { |
||||||
|
if(this.isOutOfChart() === true || this.isPanning === true || this.isBrushing === true) { |
||||||
|
this.crosshair.style('display', 'none'); |
||||||
|
return; |
||||||
|
} |
||||||
|
this.crosshair.style('display', null); |
||||||
|
} |
||||||
|
|
||||||
|
onMouseOut(): void { |
||||||
|
if(this.options.eventsCallbacks !== undefined && this.options.eventsCallbacks.mouseOut !== undefined) { |
||||||
|
this.options.eventsCallbacks.mouseOut(); |
||||||
|
} |
||||||
|
this.crosshair.style('display', 'none'); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// it is used with Vue.component, e.g.: Vue.component('chartwerk-scatter-pod', VueChartwerkScatterPodObject)
|
||||||
|
export const VueChartwerkScatterPodObject = { |
||||||
|
// alternative to `template: '<div class="chartwerk-scatter-pod" :id="id" />'`
|
||||||
|
render(createElement) { |
||||||
|
return createElement( |
||||||
|
'div', |
||||||
|
{ |
||||||
|
class: { 'chartwerk-scatter-pod': true }, |
||||||
|
attrs: { id: this.id } |
||||||
|
} |
||||||
|
); |
||||||
|
}, |
||||||
|
mixins: [VueChartwerkPodMixin], |
||||||
|
methods: { |
||||||
|
render() { |
||||||
|
if(this.pod === undefined) {
|
||||||
|
this.pod = new ChartwerkScatterPod(document.getElementById(this.id), this.series, this.options); |
||||||
|
this.pod.render(); |
||||||
|
} else { |
||||||
|
this.pod.updateData(this.series, this.options); |
||||||
|
} |
||||||
|
}, |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
export { ScatterData, ScatterOptions, TickOrientation, TimeFormat, PointType, LineType }; |
@ -0,0 +1,30 @@ |
|||||||
|
import { TimeSerie, Options } from '@chartwerk/core'; |
||||||
|
|
||||||
|
|
||||||
|
type ScatterDataParams = { |
||||||
|
pointType: PointType; |
||||||
|
lineType: LineType; |
||||||
|
pointSize: number; |
||||||
|
colorFormatter?: ColorFormatter |
||||||
|
} |
||||||
|
type ScatterOptionsParams = { |
||||||
|
voronoiRadius: number; |
||||||
|
circleView: boolean; |
||||||
|
renderGrid: boolean; |
||||||
|
} |
||||||
|
export type ScatterData = TimeSerie & Partial<ScatterDataParams>; |
||||||
|
export type ScatterOptions = Options & Partial<ScatterOptionsParams>; |
||||||
|
|
||||||
|
export enum PointType { |
||||||
|
NONE = 'none', |
||||||
|
CIRCLE = 'circle', |
||||||
|
RECTANGLE = 'rectangle' |
||||||
|
} |
||||||
|
|
||||||
|
export enum LineType { |
||||||
|
NONE = 'none', |
||||||
|
SOLID = 'solid', |
||||||
|
DASHED = 'dashed' |
||||||
|
} |
||||||
|
|
||||||
|
export type ColorFormatter = (datapoint: number[]) => string; |
@ -0,0 +1,22 @@ |
|||||||
|
{ |
||||||
|
"compilerOptions": { |
||||||
|
"target": "es5", |
||||||
|
"rootDir": "./src", |
||||||
|
"module": "esnext", |
||||||
|
"moduleResolution": "node", |
||||||
|
"declaration": true, |
||||||
|
"declarationDir": "dist", |
||||||
|
"allowSyntheticDefaultImports": true, |
||||||
|
"inlineSourceMap": false, |
||||||
|
"sourceMap": true, |
||||||
|
"noEmitOnError": false, |
||||||
|
"emitDecoratorMetadata": false, |
||||||
|
"experimentalDecorators": true, |
||||||
|
"noImplicitReturns": true, |
||||||
|
"noImplicitThis": false, |
||||||
|
"noImplicitUseStrict": false, |
||||||
|
"noImplicitAny": false, |
||||||
|
"noUnusedLocals": false, |
||||||
|
"baseUrl": "./src" |
||||||
|
} |
||||||
|
} |
Loading…
Reference in new issue