Compare commits

..

No commits in common. 'main' and 'target-master' have entirely different histories.

  1. 9
      .gitignore
  2. 786
      .yarn/releases/yarn-3.2.1.cjs
  3. 3
      .yarnrc.yml
  4. 14
      build/webpack.base.conf.js
  5. 2
      build/webpack.dev.conf.js
  6. 3
      build/webpack.prod.conf.js
  7. 45
      demo.html
  8. 15
      dist/delaunay.d.ts
  9. 88
      dist/index.d.ts
  10. 9
      dist/index.js
  11. 26
      dist/types.d.ts
  12. 4662
      package-lock.json
  13. 39
      package.json
  14. 26
      react/build/webpack.base.conf.js
  15. 8
      react/build/webpack.dev.conf.js
  16. 6
      react/build/webpack.prod.conf.js
  17. 23
      react/package.json
  18. 62
      react/src/index.tsx
  19. 23
      react/tsconfig.json
  20. 1322
      react/yarn.lock
  21. 34
      src/delaunay.ts
  22. 259
      src/index.ts
  23. 26
      src/models/scatter_series.ts
  24. 43
      src/types.ts
  25. 3
      tsconfig.json
  26. 2155
      yarn.lock

9
.gitignore vendored

@ -1,10 +1 @@
node_modules node_modules
dist
# yarn
.yarn
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
!.yarn/versions

786
.yarn/releases/yarn-3.2.1.cjs vendored

File diff suppressed because one or more lines are too long

3
.yarnrc.yml

@ -1,3 +0,0 @@
nodeLinker: node-modules
yarnPath: .yarn/releases/yarn-3.2.1.cjs

14
build/webpack.base.conf.js

@ -1,5 +1,4 @@
const path = require('path'); const path = require('path');
const CopyPlugin = require('copy-webpack-plugin');
function resolve(dir) { function resolve(dir) {
@ -9,14 +8,7 @@ function resolve(dir) {
module.exports = { module.exports = {
context: resolve('src'), context: resolve('src'),
entry: './index.ts', entry: './index.ts',
plugins: [ plugins: [],
new CopyPlugin({
patterns: [
{ from: '../react/dist/index.js', to: 'react/index.js' },
{ from: '../react/dist/index.d.ts', to: 'react/index.d.ts' },
],
})
],
module: { module: {
rules: [ rules: [
{ {
@ -33,10 +25,6 @@ module.exports = {
}, },
resolve: { resolve: {
extensions: ['.ts', '.js'], extensions: ['.ts', '.js'],
// this is necessary for resolution of external libs like d3 in dev mode
// when core is linked: webpack will take d3 from this node_modules but not from
// internal so you get one version of d3
modules: [path.resolve(__dirname, '../node_modules'), 'node_modules']
}, },
output: { output: {
filename: 'index.js', filename: 'index.js',

2
build/webpack.dev.conf.js

@ -2,7 +2,7 @@ const baseWebpackConfig = require('./webpack.base.conf');
var conf = baseWebpackConfig; var conf = baseWebpackConfig;
conf.devtool = 'inline-source-map'; conf.devtool = 'inline-source-map';
conf.watch = true;
conf.mode = 'development'; conf.mode = 'development';
conf.output.filename = 'index.dev.js';
module.exports = conf; module.exports = conf;

3
build/webpack.prod.conf.js

@ -2,8 +2,5 @@ const baseWebpackConfig = require('./webpack.base.conf');
var conf = baseWebpackConfig; var conf = baseWebpackConfig;
conf.mode = 'production'; conf.mode = 'production';
conf.externals = [
'@chartwerk/core', 'd3', 'lodash'
];
module.exports = baseWebpackConfig; module.exports = baseWebpackConfig;

45
examples/demo.html → demo.html

@ -4,31 +4,28 @@
<meta content="text/html;charset=utf-8" http-equiv="Content-Type"> <meta content="text/html;charset=utf-8" http-equiv="Content-Type">
<meta content="utf-8" http-equiv="encoding"> <meta content="utf-8" http-equiv="encoding">
<script src="../dist/index.dev.js" type="text/javascript"></script> <script src="./dist/index.js" type="text/javascript"></script>
</head> </head>
<body> <body>
<div id="chart" style="width: 500px; height: 500px;"></div> <div id="chart" style="width: 500px; height: 500px;"></div>
<script type="text/javascript"> <script type="text/javascript">
const datapoints1 = [
[100, -50, 0],
[200, 150, 0],
[100, 160, 1],
[150, 170, 1],
[150, 180, 0],
[150, 250, 1]
];
var pod = new ChartwerkScatterPod( var pod = new ChartwerkScatterPod(
document.getElementById('chart'), document.getElementById('chart'),
[ [
{ {
target: 'test1', target: 'test1',
datapoints: datapoints1, datapoints: [
[100, -50, 0],
[200, 150, 0],
[100, 160, 1],
[150, 170, 1],
[150, 180, 0],
[150, 250, 1]
],
color: 'red', color: 'red',
lineType: 'dashed', lineType: 'dashed',
pointType: 'circle', pointType: 'circle'
colorFormatter: (values, idx) => datapoints1[idx][2] === 0 ? 'blue' : 'green',
clickCallback: (metricData, pointData) => { console.log('click', metricData, pointData) }
}, },
{ {
target: 'test2', target: 'test2',
@ -40,19 +37,18 @@
color: 'purple', color: 'purple',
pointType: 'rectangle', pointType: 'rectangle',
pointSize: 5, pointSize: 5,
yOrientation: 'right',
} }
], ],
{ {
axis: { axis: {
x: { x: {
format: 'numeric', format: 'numeric',
range: [-100, 300], range: [-100, 300]
label: 'x'
}, },
y: { y: {
invert: true, invert: true,
range: [-100, 250], range: [-100, 250]
label: 'y'
}, },
y1: { y1: {
isActive: true, isActive: true,
@ -61,8 +57,8 @@
}, },
zoomEvents: { zoomEvents: {
mouse: { mouse: {
pan: { isActive: true, orientation: 'both', keyEvent: 'main' }, pan: { isActive: false, orientation: 'both', keyEvent: 'main' },
zoom: { isActive: false, keyEvent: 'shift' }, zoom: { isActive: true, keyEvent: 'shift' },
}, },
scroll: { scroll: {
pan: { isActive: false }, pan: { isActive: false },
@ -73,19 +69,18 @@
orientation: 'both', orientation: 'both',
color: 'gray' color: 'gray'
}, },
labelFormat: {
yAxis: 'y',
xAxis: 'x'
},
eventsCallbacks: { eventsCallbacks: {
zoomOut: () => { pod.render() } zoomOut: () => { pod.render() }
}, },
margin: { top: 30, right: 30, bottom: 40, left: 30 }, margin: { top: 30, right: 30, bottom: 40, left: 30 },
circleView: true,
} }
); );
pod.render(); pod.render();
</script> </script>
</body> </body>
</html> </html>
<style>
.overlay {
fill: black;
}
</style>

15
dist/delaunay.d.ts vendored

@ -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;
}

88
dist/index.d.ts vendored

@ -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 };

9
dist/index.js vendored

File diff suppressed because one or more lines are too long

26
dist/types.d.ts vendored

@ -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 {};

4662
package-lock.json generated

File diff suppressed because it is too large Load Diff

39
package.json

@ -1,35 +1,30 @@
{ {
"name": "@chartwerk/scatter-pod", "name": "@chartwerk/scatter-pod",
"version": "0.6.9", "version": "0.2.4",
"description": "Chartwerk scatter pod", "description": "Chartwerk scatter pod",
"main": "dist/index.js", "main": "dist/index.js",
"files": [
"/dist"
],
"scripts": { "scripts": {
"build": "rm -rf dist && cd react && yarn build && cd .. && webpack --config build/webpack.prod.conf.js && webpack --config build/webpack.dev.conf.js", "build": "webpack --config build/webpack.prod.conf.js",
"dev": "webpack --watch --config build/webpack.dev.conf.js", "dev": "webpack --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": {}, "repository": {},
"author": "CorpGlory", "author": "CorpGlory",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@chartwerk/core": "latest" "@chartwerk/core": "github:chartwerk/core#532eddbc8ad938091b1d9ec1693cec5eddfdbfc2"
}, },
"devDependencies": { "devDependencies": {
"copy-webpack-plugin": "^11.0.0", "@types/d3": "^5.7.2",
"css-loader": "^6.8.1", "@types/lodash": "^4.14.149",
"d3-delaunay": "^6.0.4", "css-loader": "^3.4.2",
"style-loader": "^3.3.3", "d3": "^5.15.0",
"ts-loader": "^9.4.3", "d3-delaunay": "^6.0.2",
"typescript": "^5.1.3", "lodash": "^4.17.15",
"webpack": "^5.87.0", "style-loader": "^1.1.3",
"webpack-cli": "^5.1.4" "ts-loader": "^6.2.1",
}, "typescript": "^3.8.3",
"packageManager": "yarn@3.2.1", "webpack": "^4.42.0",
"workspaces": [ "webpack-cli": "^3.3.11"
"react/*" }
]
} }

26
react/build/webpack.base.conf.js

@ -1,26 +0,0 @@
const path = require('path');
module.exports = {
entry: './src/index.tsx',
module: {
rules: [
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/,
},
],
},
resolve: {
extensions: ['.tsx', '.ts', '.js'],
},
output: {
filename: 'index.js',
path: path.resolve(__dirname, '../dist'),
libraryTarget: 'umd',
umdNamedDefine: true,
},
externals: [
'@chartwerk/scatter-pod', 'react'
]
};

8
react/build/webpack.dev.conf.js

@ -1,8 +0,0 @@
const baseWebpackConfig = require('./webpack.base.conf');
var conf = baseWebpackConfig;
conf.devtool = 'inline-source-map';
conf.mode = 'development';
conf.output.filename = 'index.dev.js';
module.exports = conf;

6
react/build/webpack.prod.conf.js

@ -1,6 +0,0 @@
const baseWebpackConfig = require('./webpack.base.conf');
var conf = baseWebpackConfig;
conf.mode = 'production';
module.exports = baseWebpackConfig;

23
react/package.json

@ -1,23 +0,0 @@
{
"name": "scatter-pod-react",
"version": "0.0.1",
"description": "React wrapper around scatter-pod",
"main": "dist/index.js",
"repository": "http://code.corpglory.net/chartwerk/scatter-pod.git",
"author": "CorpGlory Inc.",
"license": "ISC",
"scripts": {
"build": "webpack --config build/webpack.prod.conf.js",
"dev": "webpack --config build/webpack.dev.conf.js"
},
"dependencies": {
"@chartwerk/scatter-pod": "latest",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"ts-loader": "^9.5.1",
"typescript": "^5.4.3",
"webpack": "^5.87.0"
}
}

62
react/src/index.tsx

@ -1,62 +0,0 @@
import { ScatterData, ScatterOptions, ChartwerkScatterPod } from '@chartwerk/scatter-pod';
import React, { useEffect, useRef, useState } from 'react';
import _ from 'lodash';
export type ChartwerkScatterPodReactProps = {
id?: string;
series: ScatterData[];
options?: ScatterOptions;
className?: string;
}
export function ChartwerkScatterPodReact(props: ChartwerkScatterPodReactProps) {
const [pod, setPod] = useState<ChartwerkScatterPod | null>(null);
const [hack, setHack] = useState<number | null>(null);
const chartRef = useRef(null);
const chart = chartRef.current;
useEffect(() => {
// this function will be called on component unmount
return () => {
if(pod === null) { return; }
// @ts-ignore
pod.removeEventListeners();
}
}, []);
useEffect(() => {
if(chart === null) { return; }
if(pod === null) {
const newPod = new ChartwerkScatterPod(
// @ts-ignore
chart,
props.series,
props.options,
);
setPod(newPod);
newPod.render();
} else {
// TODO: actually it's wrong logic with updates
// because it creates new pod anyway
pod.updateData(props.series, props.options);
}
}, [chart, props.id, props.options]);
// TODO: it's a hack to render the ScatterPod right after the div appears in DOM
setTimeout(() => {
if(hack === null) {
setHack(1);
}
}, 1);
return (
<div id={props.id} className={props.className} ref={chartRef}></div>
);
}
export default ChartwerkScatterPodReact;

23
react/tsconfig.json

@ -1,23 +0,0 @@
{
"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",
"jsx": "react"
}
}

1322
react/yarn.lock

File diff suppressed because it is too large Load Diff

34
src/models/delaunay.ts → src/delaunay.ts

@ -1,24 +1,24 @@
import { ScatterData, PointType, DelaunayDataRow } 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
export class DelaunayDiagram { export class DelaunayDiagram {
private _delaunayData: DelaunayDataRow[]; private _delaunayData: number[][]; // [ 0:y, 1:x, ..., last:serieIdx ][]
private _delaunayDiagram: any; private _delaunayDiagram: any;
constructor( constructor(
protected series: ScatterSeries, protected series: ScatterData[],
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(): DelaunayDataRow[] | undefined { public get data(): number[][] | undefined {
if(!this._delaunayData || this._delaunayData.length === 0) { if(!this._delaunayData || this._delaunayData.length === 0) {
return undefined; return undefined;
} }
@ -33,8 +33,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: DelaunayDataRow) => xScale(d[0]), (d: number[]) => xScale(d[1]),
(d: DelaunayDataRow) => yScale(this.series.getSerieByTarget(d[4])?.yOrientation)(d[1]), (d: number[]) => yScale(this.series[_.last(d)].yOrientation)(d[0]),
); );
console.timeEnd('delaunay-init'); console.timeEnd('delaunay-init');
} }
@ -51,29 +51,39 @@ export class DelaunayDiagram {
return pointIndex; return pointIndex;
} }
public getDataRowByIndex(index: number): DelaunayDataRow | undefined { public getDataRowByIndex(index: number): number[] | undefined {
if(!this.data) { if(!this.data) {
return undefined; return undefined;
} }
return this.data[index]; return this.data[index];
} }
private getDatapointsForDelaunay(): DelaunayDataRow[] | undefined { private getDatapointsForDelaunay(): number[][] | 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.visibleSeries.filter((serie: ScatterData) => serie.pointType !== PointType.NONE); const seriesForPointType = this.series.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[]): DelaunayDataRow[] { private concatSeriesDatapoints(series: ScatterData[]): number[][] {
// return type row: [ 0:y, 1:x, 2?:custom value, last:serieIdx ]
const datapointsList = _.map(series, serie => { const datapointsList = _.map(series, serie => {
const datapointsWithOptions = _.map(serie.datapoints, (row, rowIdx) => [row[0], row[1], row[2] || null, rowIdx, serie.target]); const serieIdx = this.getSerieIdxByTarget(serie.target);
const datapointsWithOptions = _.map(serie.datapoints, row => _.concat(row, serieIdx));
return datapointsWithOptions; return datapointsWithOptions;
}); });
return _.union(...datapointsList); 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;
}
} }

259
src/index.ts

@ -1,65 +1,74 @@
import { ChartwerkPod, VueChartwerkPodMixin, TimeFormat, yAxisOrientation, CrosshairOrientation } from '@chartwerk/core'; import { ChartwerkPod, VueChartwerkPodMixin, TickOrientation, TimeFormat, yAxisOrientation, CrosshairOrientation, PanOrientation } from '@chartwerk/core';
import { ScatterData, ScatterOptions, PointType, LineType, HighlightedData, MouseMoveEvent, DelaunayDataRow } from './types'; import { ScatterData, ScatterOptions, PointType, LineType, ColorFormatter } from './types';
import { DelaunayDiagram } from './models/delaunay'; import { DelaunayDiagram } from './delaunay';
import { ScatterSeries } from './models/scatter_series';
import * as d3 from 'd3'; import * as d3 from 'd3';
import * as _ from 'lodash'; import * as _ from 'lodash';
// TODO: use pod state with defaults
const DEFAULT_POINT_SIZE = 4;
const POINT_HIGHLIGHT_DIAMETER = 4; const POINT_HIGHLIGHT_DIAMETER = 4;
const CROSSHAIR_BACKGROUND_OPACITY = 0.3; const CROSSHAIR_BACKGROUND_OPACITY = 0.3;
const DEFAULT_POINT_TYPE = PointType.CIRCLE;
const DEFAULT_LINE_TYPE = LineType.NONE;
const DEFAULT_LINE_DASHED_AMOUNT = 4; const DEFAULT_LINE_DASHED_AMOUNT = 4;
export class ChartwerkScatterPod extends ChartwerkPod<ScatterData, ScatterOptions> { export class ChartwerkScatterPod extends ChartwerkPod<ScatterData, ScatterOptions> {
metricContainer: any; _metricsContainer: 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(d3, el, _series, _options);
this.series = new ScatterSeries(_series);
} }
renderMetrics(): void { renderMetrics(): void {
if(!this.series.isSeriesAvailable) { if(this.series.length === 0) {
this.renderNoDataPointsMessage(); this.renderNoDataPointsMessage();
return; return;
} }
this.updateCrosshair(); this.updateCrosshair();
this.renderMetricContainer();
this._delaunayDiagram = new DelaunayDiagram(this.series, this.state.xScale, this.getYScale.bind(this)); this._delaunayDiagram = new DelaunayDiagram(this.series, this.xScale, this.getYScale.bind(this));
this.renderLines(); this.renderLines();
this.renderPoints(); this.renderPoints();
} }
clearAllMetrics(): void { renderMetricContainer(): void {
// TODO: temporary hack before it will be implemented in core. // container for clip path
this.chartContainer.selectAll('.metric-el').remove(); 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 { protected updateCrosshair(): void {
this.crosshair.selectAll('circle').remove();
// TODO: Crosshair class, which can be used as Pod // TODO: Crosshair class, which can be used as Pod
this.appendCrosshairPoints(); this.appendCrosshairPoints();
} }
appendCrosshairPoints(): void { appendCrosshairPoints(): void {
this.series.visibleSeries.forEach((serie: ScatterData) => { this.series.forEach((serie: ScatterData, serieIdx: number) => {
this.appendCrosshairPoint(serie.idx, serie.pointType, serie.pointSize); this.appendCrosshairPoint(serieIdx);
}); });
} }
protected appendCrosshairPoint(serieIdx: number, pointType: PointType, size: number): void { protected appendCrosshairPoint(serieIdx: number): void {
// TODO: add Crosshair type options // TODO: add Crosshair type options
const pointType = this.series[serieIdx].pointType || DEFAULT_POINT_TYPE;
switch(pointType) { switch(pointType) {
case PointType.NONE: case PointType.NONE:
return; return;
case PointType.CIRCLE: case PointType.CIRCLE:
this.crosshair.append('circle') this.crosshair.append('circle')
.attr('class', `crosshair-point crosshair-point-${serieIdx} crosshair-background`) .attr('class', `crosshair-point crosshair-point-${serieIdx} crosshair-background`)
.attr('r', this.getCrosshairCircleBackgroundSize(size, pointType)) .attr('r', this.getCrosshairCircleBackgroundSize(serieIdx))
.attr('clip-path', `url(#${this.rectClipId})`) .attr('clip-path', `url(#${this.rectClipId})`)
.style('opacity', CROSSHAIR_BACKGROUND_OPACITY) .style('opacity', CROSSHAIR_BACKGROUND_OPACITY)
.style('pointer-events', 'none') .style('pointer-events', 'none')
@ -68,8 +77,8 @@ export class ChartwerkScatterPod extends ChartwerkPod<ScatterData, ScatterOption
case PointType.RECTANGLE: case PointType.RECTANGLE:
this.crosshair.append('rect') this.crosshair.append('rect')
.attr('class', `crosshair-point crosshair-point-${serieIdx} crosshair-background`) .attr('class', `crosshair-point crosshair-point-${serieIdx} crosshair-background`)
.attr('width', this.getCrosshairCircleBackgroundSize(size, pointType)) .attr('width', this.getCrosshairCircleBackgroundSize(serieIdx))
.attr('height', this.getCrosshairCircleBackgroundSize(size, pointType)) .attr('height', this.getCrosshairCircleBackgroundSize(serieIdx))
.attr('clip-path', `url(#${this.rectClipId})`) .attr('clip-path', `url(#${this.rectClipId})`)
.style('opacity', CROSSHAIR_BACKGROUND_OPACITY) .style('opacity', CROSSHAIR_BACKGROUND_OPACITY)
.style('pointer-events', 'none') .style('pointer-events', 'none')
@ -81,97 +90,82 @@ export class ChartwerkScatterPod extends ChartwerkPod<ScatterData, ScatterOption
} }
protected renderLines(): void { protected renderLines(): void {
this.series.visibleSeries.forEach(serie => { this.series.forEach((serie, serieIdx) => {
this.renderLine(serie); if(serie.visible === false) {
return;
}
const lineType = serie.lineType || DEFAULT_LINE_TYPE;
this.renderLine(serie.datapoints, lineType, this.getSerieColor(serieIdx), serie.yOrientation);
}); });
} }
renderLine(serie: ScatterData): void { renderLine(datapoints: number[][], lineType: LineType, color: string, orientation: yAxisOrientation): void {
if(serie.lineType === LineType.NONE) { if(lineType === LineType.NONE) {
return; return;
} }
let strokeDasharray; let strokeDasharray;
// TODO: move to option // TODO: move to option
if(serie.lineType === LineType.DASHED) { if(lineType === LineType.DASHED) {
strokeDasharray = DEFAULT_LINE_DASHED_AMOUNT; strokeDasharray = DEFAULT_LINE_DASHED_AMOUNT;
} }
const lineGenerator = d3.line() const lineGenerator = this.d3.line()
.x((d: [number, number]) => this.state.xScale(d[0])) .x((d: [number, number]) => this.xScale(d[1]))
.y((d: [number, number]) => this.getYScale(serie.yOrientation)(d[1])); .y((d: [number, number]) => this.getYScale(orientation)(d[0]));
this.metricContainer this._metricsContainer
.append('path') .append('path')
.datum(serie.datapoints) .datum(datapoints)
.attr('class', 'metric-path') .attr('class', 'metric-path')
.attr('d', lineGenerator)
.attr('fill', 'none') .attr('fill', 'none')
.attr('stroke', serie.color) .style('pointer-events', 'none')
.attr('stroke', color)
.attr('stroke-width', 1) .attr('stroke-width', 1)
.attr('stroke-opacity', 0.7) .attr('stroke-opacity', 0.7)
.attr('stroke-dasharray', strokeDasharray) .attr('stroke-dasharray', strokeDasharray)
.style('pointer-events', serie.clickCallback ? 'auto' : 'none') .attr('d', lineGenerator);
.style('cursor', serie.clickCallback ? 'pointer' : 'crosshair')
.on('click', () => { serie.clickCallback({ target: serie.target, class: serie.class, alias: serie.alias }) });
} }
protected renderPoints(): void { protected renderPoints(): void {
if(!this._delaunayDiagram.data) { if(!this._delaunayDiagram.data) {
return; return;
} }
this.metricContainer.selectAll(null)
this._metricsContainer.selectAll(null)
.data(this._delaunayDiagram.data) .data(this._delaunayDiagram.data)
.enter() .enter()
.append('circle') .append('circle')
.filter((d: DelaunayDataRow) => this.series.getSerieByTarget(d[4])?.pointType !== PointType.RECTANGLE) .filter((d: number[]) => this.series[_.last(d)].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: DelaunayDataRow) => this.series.getSerieByTarget(d[4])?.pointSize) .attr('r', (d: number[]) => this.series[_.last(d)].pointSize || DEFAULT_POINT_SIZE)
.attr('cx', (d: DelaunayDataRow) => this.state.xScale(d[0])) .style('fill', (d: number[]) => this.getSerieColor(_.last(d)))
.attr('cy', (d: DelaunayDataRow) => this.getYScale(this.series.getSerieByTarget(d[4])?.yOrientation)(d[1])) .style('pointer-events', 'none')
.style('fill', (d: DelaunayDataRow, i: number) => this.getSeriesColorFromDataRow(d, i)) .attr('cx', (d: any[]) => this.xScale(d[1]))
.style('pointer-events', (d: DelaunayDataRow) => this.series.getSerieByTarget(d[4])?.clickCallback ? 'auto' : 'none') .attr('cy', (d: any[]) => this.getYScale(this.series[_.last(d)].yOrientation)(d[0]));
.style('cursor', (d: DelaunayDataRow) => this.series.getSerieByTarget(d[4])?.clickCallback ? 'pointer' : 'crosshair')
.on('click', (d: DelaunayDataRow) => {
d3.event.stopPropagation();
const serie = this.series.getSerieByTarget(d[4]);
const serieData = { target: serie?.target, class: serie?.class, alias: serie?.alias };
serie?.clickCallback(serieData, d);
});
this.metricContainer.selectAll(null) this._metricsContainer.selectAll(null)
.data(this._delaunayDiagram.data) .data(this._delaunayDiagram.data)
.enter() .enter()
.append('rect') .append('rect')
.filter((d: DelaunayDataRow) => this.series.getSerieByTarget(d[4])?.pointType === PointType.RECTANGLE) .filter((d: number[]) => this.series[_.last(d)].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: DelaunayDataRow) => this.series.getSerieByTarget(d[4])?.pointSize) .attr('r', (d: number[]) => this.series[_.last(d)].pointSize || DEFAULT_POINT_SIZE)
.attr('x', (d: DelaunayDataRow) => this.state.xScale(d[0]) - this.series.getSerieByTarget(d[4])?.pointSize / 2) .style('fill', (d: number[]) => this.getSerieColor(_.last(d)))
.attr('y', (d: DelaunayDataRow) => this.getYScale(this.series.getSerieByTarget(d[4])?.yOrientation)(d[1]) - this.series.getSerieByTarget(d[4])?.pointSize / 2) .style('pointer-events', 'none')
.attr('width', (d: DelaunayDataRow) => this.series.getSerieByTarget(d[4])?.pointSize) .attr('x', (d: number[]) => this.xScale(d[1]) - (this.series[_.last(d)].pointSize || DEFAULT_POINT_SIZE) / 2)
.attr('height', (d: DelaunayDataRow) => this.series.getSerieByTarget(d[4])?.pointSize) .attr('y', (d: number[]) => this.getYScale(this.series[_.last(d)].yOrientation)(d[0]) - (this.series[_.last(d)].pointSize || DEFAULT_POINT_SIZE) / 2)
.style('fill', (d: DelaunayDataRow) => this.series.getSerieByTarget(d[4])?.color) .attr('width', (d: number[]) => this.series[_.last(d)].pointSize || DEFAULT_POINT_SIZE)
.style('pointer-events', (d: DelaunayDataRow) => this.series.getSerieByTarget(d[4])?.clickCallback ? 'auto' : 'none') .attr('height', (d: number[]) => this.series[_.last(d)].pointSize || DEFAULT_POINT_SIZE);
.style('cursor', (d: DelaunayDataRow) => this.series.getSerieByTarget(d[4])?.clickCallback ? 'pointer' : 'crosshair')
.on('click', (d: DelaunayDataRow) => {
d3.event.stopPropagation();
const serie = this.series.getSerieByTarget(d[4]);
const serieData = { target: serie?.target, class: serie?.class, alias: serie?.alias };
serie?.clickCallback(serieData, d);
});
}
getSeriesColorFromDataRow(values: DelaunayDataRow, rowIdx: number): string {
const serie = this.series.getSerieByTarget(values[4]);
if(serie?.colorFormatter) {
return serie.colorFormatter(values, rowIdx);
}
return serie.color;
} }
onPanningEnd(): void { onPanningEnd(): void {
this.isPanning = false; this.isPanning = false;
this.onMouseOut(); this.onMouseOut();
this._delaunayDiagram.setDelaunayDiagram(this.state.xScale, this.getYScale.bind(this)); this._delaunayDiagram.setDelaunayDiagram(this.xScale, this.getYScale.bind(this));
this.options.callbackPanningEnd([this.state.xValueRange, this.state.yValueRange, this.state.y1ValueRange]); 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 { unhighlight(): void {
@ -181,37 +175,47 @@ export class ChartwerkScatterPod extends ChartwerkPod<ScatterData, ScatterOption
highlight(pointIdx: number): void { highlight(pointIdx: number): void {
this.unhighlight(); this.unhighlight();
const row = this._delaunayDiagram.getDataRowByIndex(pointIdx); const datapoint = this._delaunayDiagram.getDataRowByIndex(pointIdx);
if(row === undefined || row === null) { if(datapoint === undefined || datapoint === null) {
return; return;
} }
const serie = this.series.getSerieByTarget(row[4]);
const size = this.getCrosshairCircleBackgroundSize(serie.pointSize, serie.pointType); const serieIdx = _.last(datapoint);
this.crosshair.selectAll(`.crosshair-point-${serie.idx}`) const serieOrientation = this.series[serieIdx].yOrientation;
.attr('cx', this.state.xScale(row[0])) const size = this.getCrosshairCircleBackgroundSize(serieIdx);
.attr('cy', this.getYScale(serie.yOrientation)(row[1])) const colorFormatter = this.series[serieIdx].colorFormatter;
.attr('x', this.state.xScale(row[0]) - size / 2) this.crosshair.selectAll(`.crosshair-point-${serieIdx}`)
.attr('y', this.getYScale(serie.yOrientation)(row[1]) - size / 2) .attr('cx', this.xScale(datapoint[1]))
.attr('fill', this.getSeriesColorFromDataRow(row, pointIdx)) .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); .style('display', null);
} }
protected getCrosshairCircleBackgroundSize(pointSize: number, pointType: PointType): number { 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; let highlightDiameter = POINT_HIGHLIGHT_DIAMETER;
if(pointType === PointType.RECTANGLE) { if(pointType === PointType.RECTANGLE) {
highlightDiameter = highlightDiameter * 2; highlightDiameter = highlightDiameter * 2;
} }
return pointSize + highlightDiameter; return seriePointSize + highlightDiameter;
} }
public renderSharedCrosshair(values: { x?: number, y?: number }): void { public renderSharedCrosshair(values: { x?: number, y?: number }): void {
this.onMouseOver(); // TODO: refactor to use it once this.onMouseOver(); // TODO: refactor to use it once
const eventX = this.state.xScale(values.x); const eventX = this.xScale(values.x);
const eventY = this.state.yScale(values.y); const eventY = this.yScale(values.y);
this.moveCrosshairLine(eventX, eventY); this.moveCrosshairLine(eventX, eventY);
const datapoints = this.findAndHighlightDatapoints(values.x, values.y); const datapoints = this.findAndHighlightDatapoints(values.x, values.y);
this.options.callbackSharedCrosshairMove({ 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, datapoints,
eventX, eventY eventX, eventY
}); });
@ -242,44 +246,33 @@ export class ChartwerkScatterPod extends ChartwerkPod<ScatterData, ScatterOption
} }
} }
findAndHighlightDatapoints(eventX: number, eventY: number): { findAndHighlightDatapoints(eventX: number, eventY: number): { values: any[], pointIdx: number } | null {
xValue: number, yValue: number, customValue: number, if(this.series === undefined || this.series.length === 0) {
pointIdx: number, totalPointIdx: number,
serieInfo: { target: string, alias?: string, class?: string, idx?: number }
} | null {
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 {
xValue: row[0], values: this._delaunayDiagram.data[pointIndex],
yValue: row[1], pointIdx: pointIndex,
customValue: row[2],
pointIdx: row[3],
serieInfo: {
target: serie.target,
alias: serie.alias,
class: serie.class,
idx: serie.idx,
},
totalPointIdx: pointIndex,
}; };
} }
protected getYScale(orientation: yAxisOrientation): d3.ScaleLinear<number, number> { protected getYScale(orientation: yAxisOrientation): d3.ScaleLinear<number, number> {
if(orientation === undefined || orientation === yAxisOrientation.BOTH) {
return this.yScale;
}
switch(orientation) { switch(orientation) {
case yAxisOrientation.LEFT: case yAxisOrientation.LEFT:
return this.state.yScale; return this.yScale;
case yAxisOrientation.RIGHT: case yAxisOrientation.RIGHT:
return this.state.y1Scale; return this.y1Scale;
default: default:
throw new Error(`Unknown type of y axis orientation: ${orientation}`) throw new Error(`Unknown type of y axis orientation: ${orientation}`)
} }
@ -290,11 +283,12 @@ export class ChartwerkScatterPod extends ChartwerkPod<ScatterData, ScatterOption
} }
onMouseMove(): void { onMouseMove(): void {
const mousePosition = d3.mouse(this.chartContainer.node()); const mousePosition = this.d3.mouse(this.chartContainer.node());
const eventX = mousePosition[0]; const eventX = mousePosition[0];
const eventY = mousePosition[1]; const eventY = mousePosition[1];
if(this.isPanning === true || this.isBrushing === true) { // TODO: seems isOutOfChart is deprecated (check clippath correctness)
if(this.isOutOfChart() === true || this.isPanning === true || this.isBrushing === true) {
this.crosshair.style('display', 'none'); this.crosshair.style('display', 'none');
return; return;
} else { } else {
@ -305,25 +299,24 @@ 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);
this.options.callbackMouseMove({ if(this.options.eventsCallbacks === undefined || this.options.eventsCallbacks.mouseMove === undefined) {
bbox: { console.log('Mouse move, but there is no callback');
clientX: d3.event.clientX, return;
clientY: d3.event.clientY, }
x: eventX, // TODO: group fields
y: eventY, this.options.eventsCallbacks.mouseMove({
chartWidth: this.width, x: this.d3.event.clientX,
chartHeight: this.height, y: this.d3.event.clientY,
}, xval: this.xScale.invert(eventX),
data: { yval: this.xScale.invert(eventY),
xval: this.state.xScale.invert(eventX), highlighted,
yval: this.state.xScale.invert(eventY), chartX: eventX,
highlighted, chartWidth: this.width
}
}); });
} }
onMouseOver(): void { onMouseOver(): void {
if(this.isPanning === true || this.isBrushing === true) { if(this.isOutOfChart() === true || this.isPanning === true || this.isBrushing === true) {
this.crosshair.style('display', 'none'); this.crosshair.style('display', 'none');
return; return;
} }
@ -331,7 +324,9 @@ export class ChartwerkScatterPod extends ChartwerkPod<ScatterData, ScatterOption
} }
onMouseOut(): void { onMouseOut(): void {
this.options.callbackMouseOut(); if(this.options.eventsCallbacks !== undefined && this.options.eventsCallbacks.mouseOut !== undefined) {
this.options.eventsCallbacks.mouseOut();
}
this.crosshair.style('display', 'none'); this.crosshair.style('display', 'none');
} }
} }
@ -361,4 +356,4 @@ export const VueChartwerkScatterPodObject = {
} }
}; };
export { ScatterData, ScatterOptions, TimeFormat, PointType, LineType, HighlightedData, MouseMoveEvent }; export { ScatterData, ScatterOptions, TickOrientation, TimeFormat, PointType, LineType };

26
src/models/scatter_series.ts

@ -1,26 +0,0 @@
import { CoreSeries } from '@chartwerk/core';
import { ScatterData, PointType, LineType, ScatterDataParams } from '../types';
import * as _ from 'lodash';
const DEFAULT_POINT_SIZE = 4;
const SCATTER_DATA_DEFAULTS: ScatterDataParams = {
pointType: PointType.CIRCLE,
lineType: LineType.NONE,
pointSize: DEFAULT_POINT_SIZE,
colorFormatter: undefined,
clickCallback: undefined,
};
export class ScatterSeries extends CoreSeries<ScatterData> {
constructor(series: ScatterData[]) {
super(series, _.cloneDeep(SCATTER_DATA_DEFAULTS));
}
// move to parent
public getSerieByTarget(target: string): ScatterData | undefined {
return _.find(this.visibleSeries, serie => serie.target === target);
}
}

43
src/types.ts

@ -1,18 +1,18 @@
import { Serie, Options } from '@chartwerk/core'; import { TimeSerie, Options } from '@chartwerk/core';
export type ScatterDataParams = { type ScatterDataParams = {
pointType: PointType; pointType: PointType;
lineType: LineType; lineType: LineType;
pointSize: number; pointSize: number;
colorFormatter?: ColorFormatter; colorFormatter?: ColorFormatter
clickCallback?: ( metricData: { target: Target, class: string, alias: string }, pointData?: DelaunayDataRow) => void;
} }
type ScatterOptionsParams = { type ScatterOptionsParams = {
// TODO: this options is not used anywhere, let's remove it
voronoiRadius: number; voronoiRadius: number;
circleView: boolean;
renderGrid: boolean;
} }
export type ScatterData = Serie & Partial<ScatterDataParams>; export type ScatterData = TimeSerie & Partial<ScatterDataParams>;
export type ScatterOptions = Options & Partial<ScatterOptionsParams>; export type ScatterOptions = Options & Partial<ScatterOptionsParams>;
export enum PointType { export enum PointType {
@ -27,33 +27,4 @@ export enum LineType {
DASHED = 'dashed' DASHED = 'dashed'
} }
export type ColorFormatter = (datapointsRow: any[], pointIdx) => string; export type ColorFormatter = (datapoint: number[]) => 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 }
}
type Value = number;
type PointIdx = number;
type Target = string;
// type row: [ 0:x, 1:y, 2:(custom value | null), 3:pointIdx, 4:serie.target ]
export type DelaunayDataRow = [Value, Value, Value | null, PointIdx, Target];

3
tsconfig.json

@ -18,6 +18,5 @@
"noImplicitAny": false, "noImplicitAny": false,
"noUnusedLocals": false, "noUnusedLocals": false,
"baseUrl": "./src" "baseUrl": "./src"
}, }
"include": ["src/**/*"]
} }

2155
yarn.lock

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save