Compare commits

..

5 Commits

  1. 2
      .gitignore
  2. 11
      README.md
  3. 42
      examples/area.html
  4. 51
      examples/markers.html
  5. 47
      examples/markers_select.html
  6. 33
      examples/mouse_click.html
  7. 29
      examples/mouse_move.html
  8. 33
      examples/right_click.html
  9. 41
      examples/segments.html
  10. 46
      examples/segments_select.html
  11. 37
      examples/zoom_out.html
  12. 31
      package.json
  13. 0
      packages/line-pod/LICENSE
  14. 10
      packages/line-pod/build/webpack.base.conf.js
  15. 0
      packages/line-pod/build/webpack.dev.conf.js
  16. 0
      packages/line-pod/build/webpack.prod.conf.js
  17. 0
      packages/line-pod/examples/demo.html
  18. 0
      packages/line-pod/examples/demo_live.html
  19. 0
      packages/line-pod/examples/demo_vertical.html
  20. 33
      packages/line-pod/package.json
  21. 256
      packages/line-pod/src/index.ts
  22. 5
      packages/line-pod/src/models/line_series.ts
  23. 13
      packages/line-pod/src/types.ts
  24. 3
      packages/line-pod/tsconfig.json
  25. 27
      packages/react/README.md
  26. 13
      packages/react/index.html
  27. 126
      packages/react/lib/components/ChartwerkLinePod.tsx
  28. 1
      packages/react/lib/main.ts
  29. 36
      packages/react/package.json
  30. 1
      packages/react/public/vite.svg
  31. 36
      packages/react/src/App.tsx
  32. 10
      packages/react/src/main.tsx
  33. 1
      packages/react/src/vite-env.d.ts
  34. 6
      packages/react/tsconfig-build.json
  35. 25
      packages/react/tsconfig.json
  36. 10
      packages/react/tsconfig.node.json
  37. 29
      packages/react/vite.config.ts
  38. 26
      react/build/webpack.base.conf.js
  39. 8
      react/build/webpack.dev.conf.js
  40. 6
      react/build/webpack.prod.conf.js
  41. 23
      react/package.json
  42. 74
      react/src/index.tsx
  43. 1322
      react/yarn.lock
  44. 139
      src/components/markers.ts
  45. 73
      src/components/segments.ts
  46. 19
      src/models/marker.ts
  47. 11
      src/models/segment.ts
  48. 41
      src/types.ts
  49. 23
      tsconfig.json
  50. 3199
      yarn.lock

2
.gitignore vendored

@ -2,7 +2,7 @@ node_modules
dist
# yarn
.yarn
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases

11
README.md

@ -19,11 +19,12 @@
```
#### Other examples
* [Basic](https://code.corpglory.net/chartwerk/line-pod/src/branch/main/examples/basic.html)
* [Live](https://code.corpglory.net/chartwerk/line-pod/src/branch/main/examples/live.html)
* [Vertical](https://code.corpglory.net/chartwerk/line-pod/src/branch/main/examples/vertical.html)
* [Segments](https://code.corpglory.net/chartwerk/line-pod/src/branch/main/examples/segments.html)
* [Markers](https://code.corpglory.net/chartwerk/line-pod/src/branch/main/examples/markers.html)
[Static Chart](https://code.corpglory.net/chartwerk/line-pod/src/branch/main/examples/demo.html)
[Live Chart](https://code.corpglory.net/chartwerk/line-pod/src/branch/main/examples/demo_live.html)
[Vertical Chart](https://code.corpglory.net/chartwerk/line-pod/src/branch/main/examples/demo_vertical.html)
### Development

42
examples/area.html

@ -1,42 +0,0 @@
<!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.dev.js" type="text/javascript"></script>
</head>
<body>
<div id="chart" style="width: 50%; height: 500px;"></div>
<script type="text/javascript">
let options = {
renderLegend: false, usePanning: false,
axis: {
x: { format: 'numeric', range: [0, 100] },
y: { invert: true, range: [0, 100] },
y1: { isActive: true, format: 'numeric', range: [0, 1000] },
},
zoomEvents: {
mouse: { zoom: { isActive: false, orientation: 'horizontal' } },
scroll: { zoom: { isActive: false, orientation: 'horizontal' } }
},
}
const data1 = [[0,0], [35, 40], [65, 60], [100, 100]];
const data2 = [[0,0], [35, 50], [65, 65], [80, 100]];
const data3 = [[0,0], [35, 20], [65, 50], [100, 80]];
const data4 = [[0,900], [35, 800], [65, 700], [100, 600]];
var pod = new LinePod(
document.getElementById('chart'),
[
{ target: 'test1', datapoints: data1, color: 'green', renderArea: 'Above' },
{ target: 'test2', datapoints: data2, color: 'blue', renderArea: 'Below' },
{ target: 'test3', datapoints: data3, color: 'orange', renderArea: 'Below', yOrientation: 'right' },
{ target: 'test4', datapoints: data4, color: 'purple', renderArea: 'Above', yOrientation: 'right' },
],
options
);
pod.render();
</script>
</body>
</html>

51
examples/markers.html

@ -1,51 +0,0 @@
<!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.dev.js" type="text/javascript"></script>
</head>
<body>
<div id="chart" style="width: 100%; height: 500px;"></div>
<script type="text/javascript">
const startTime = 1701790172908;
const timeSerieData = [5, 6, 3, 7, 5, 6, 8, 4, 5, 6, 4, 3, 5, 7, 8]
.map((el, idx) => [startTime + idx * 1000, el]);
const markersData1 = [3, 6, 9].map(el => ({
x: startTime + el * 1000,
color: 'red',
alwaysDisplay: false,
html: new Date(startTime).toISOString(),
}));
const markersData2 = [4, 11].map(el => ({
x: startTime + el * 1000,
color: 'blue',
alwaysDisplay: true,
html: new Date(startTime).toISOString(),
}));
let options = {
renderLegend: false,
axis: {
y: { range: [0, 10] },
x: { format: 'time' }
},
}
var pod = new LinePod(
document.getElementById('chart'),
[
{ datapoints: timeSerieData, color: 'black' },
],
options,
{
series: [
{ data: markersData1 },
{ data: markersData2 },
]
}
);
pod.render();
</script>
</body>
</html>

47
examples/markers_select.html

@ -1,47 +0,0 @@
<!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.dev.js" type="text/javascript"></script>
</head>
<body>
<div id="chart" style="width: 100%; height: 500px;"></div>
<script type="text/javascript">
const startTime = 1701790172908;
const timeSerieData = [5, 6, 3, 7, 5, 6, 8, 4, 5, 6, 4, 3, 5, 7, 8]
.map((el, idx) => [startTime + idx * 1000, el]);
const markersData = [3, 6, 9].map(el => ({
x: startTime + el * 1000,
payload: el,
color: 'red',
}));
let options = {
renderLegend: false,
axis: {
y: { range: [0, 10] },
x: { format: 'time' }
},
}
var pod = new LinePod(
document.getElementById('chart'),
[
{ datapoints: timeSerieData, color: 'black' },
],
options,
{
series: [
{ data: markersData },
],
events: {
onMouseMove: (el) => { console.log(el); },
onMouseOut: () => { console.log('mouse out'); }
}
}
);
pod.render();
</script>
</body>
</html>

33
examples/mouse_click.html

@ -1,33 +0,0 @@
<!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.dev.js" type="text/javascript"></script>
</head>
<body>
<div id="chart" style="width: 50%; height: 500px;"></div>
<script type="text/javascript">
const startTime = 1590590148;
const data = Array.from(
{ length: 20 },
(el, idx) => [startTime + idx * 10000, Math.floor(Math.random() * 30)]
);
let options = {
renderLegend: false, usePanning: false,
axis: { y: { range: [0, 50] } },
zoomEvents: { mouse: {
zoom: { isActive: false },
pan: { isActive: false },
} },
eventsCallbacks: { mouseClick: console.log }
}
var pod = new LinePod(
document.getElementById('chart'),
[{ datapoints: data }],
options
);
pod.render();
</script>
</body>
</html>

29
examples/mouse_move.html

@ -1,29 +0,0 @@
<!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.dev.js" type="text/javascript"></script>
</head>
<body>
<div id="chart" style="width: 50%; height: 500px;"></div>
<script type="text/javascript">
const startTime = 1590590148;
const data = Array.from(
{ length: 20 },
(el, idx) => [startTime + idx * 10000, Math.floor(Math.random() * 30)]
);
let options = {
renderLegend: false, usePanning: false,
axis: { y: { range: [0, 50] } },
eventsCallbacks: { mouseMove: console.log }
}
var pod = new LinePod(
document.getElementById('chart'),
[{ datapoints: data }],
options
);
pod.render();
</script>
</body>
</html>

33
examples/right_click.html

@ -1,33 +0,0 @@
<!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.dev.js" type="text/javascript"></script>
</head>
<body>
<div id="chart" style="width: 50%; height: 500px;"></div>
<script type="text/javascript">
const startTime = 1590590148;
const data = Array.from(
{ length: 20 },
(el, idx) => [startTime + idx * 10000, Math.floor(Math.random() * 30)]
);
let options = {
renderLegend: false, usePanning: false,
axis: { y: { range: [0, 50] } },
zoomEvents: { mouse: {
zoom: { isActive: false },
pan: { isActive: false },
} },
eventsCallbacks: { contextMenu: (position) => console.log('contextMenu', position) }
}
var pod = new LinePod(
document.getElementById('chart'),
[{ datapoints: data }],
options
);
pod.render();
</script>
</body>
</html>

41
examples/segments.html

@ -1,41 +0,0 @@
<!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.dev.js" type="text/javascript"></script>
</head>
<body>
<div id="chart" style="width: 100%; height: 500px;"></div>
<script type="text/javascript">
const startTime = 1701790172908;
const timeSerieData = [5, 6, 3, 7, 5, 6, 8, 4, 5, 6, 4, 3, 5, 7, 8]
.map((el, idx) => [startTime + idx * 1000, el]);
const segmentsData = [3, 6, 9].map(el => [startTime + el * 1000, startTime + (el + 1) * 1100]);
let options = {
renderLegend: false,
axis: {
y: { range: [0, 10] },
x: { format: 'time' }
},
}
var pod = new LinePod(
document.getElementById('chart'),
[
{ datapoints: timeSerieData, color: 'black' },
],
options,
undefined,
[
{
data: segmentsData,
color:'#FFE545'
}
]
);
pod.render();
</script>
</body>
</html>

46
examples/segments_select.html

@ -1,46 +0,0 @@
<!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.dev.js" type="text/javascript"></script>
</head>
<body>
<div id="chart" style="width: 100%; height: 500px;"></div>
<script type="text/javascript">
const startTime = 1701790172908;
const timeSerieData = [5, 6, 3, 7, 5, 6, 8, 4, 5, 6, 4, 3, 5, 7, 8]
.map((el, idx) => [startTime + idx * 1000, el]);
const segmentsData = [3, 6, 9].map(el => [startTime + el * 1000, startTime + (el + 1) * 1100]);
let options = {
renderLegend: false,
axis: {
y: { range: [0, 10] },
x: { format: 'time' }
},
}
var pod = new LinePod(
document.getElementById('chart'),
[
{ datapoints: timeSerieData, color: 'black' },
],
options,
undefined,
[
{
data: segmentsData,
color:'#FFE545',
select: true,
opacity: 0.4,
opacitySelect: 0.8,
onSelect: console.log,
onUnselect: console.log
}
]
);
pod.render();
</script>
</body>
</html>

37
examples/zoom_out.html

@ -1,37 +0,0 @@
<!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.dev.js" type="text/javascript"></script>
</head>
<body>
<div id="chart" style="width: 50%; height: 500px;"></div>
<script type="text/javascript">
const startTime = 1590590148;
const data = Array.from(
{ length: 20 },
(el, idx) => [startTime + idx * 10000, Math.floor(Math.random() * 30)]
);
let options = {
renderLegend: false, usePanning: false,
axis: { y: { range: [0, 50] } },
zoomEvents: { mouse: {
zoom: { isActive: true, orientation: "horizontal" },
pan: { isActive: false },
}},
events: {
zoomOut: function(centers, ranges) {
console.log('zoomOut', centers, ranges);
}
}
}
var pod = new LinePod(
document.getElementById('chart'),
[{ datapoints: data }],
options
);
pod.render();
</script>
</body>
</html>

31
package.json

@ -1,37 +1,16 @@
{
"name": "@chartwerk/line-pod",
"version": "0.7.8",
"version": "0.6.9",
"description": "Chartwerk line chart",
"main": "dist/index.js",
"files": [
"/dist"
"workspaces": [
"packages/line-pod",
"packages/react"
],
"scripts": {
"build": "rm -rf dist && cd react && yarn build && cd .. && webpack --config build/webpack.prod.conf.js && webpack --config build/webpack.dev.conf.js",
"dev": "webpack --watch --config build/webpack.dev.conf.js",
"test": "echo \"Error: no test specified\" && exit 1",
"update-core": "yarn up @chartwerk/core && yarn up @chartwerk/core@latest"
},
"repository": {
"type": "git",
"url": "http://code.corpglory.net/chartwerk/line-pod.git"
},
"author": "CorpGlory",
"license": "ISC",
"dependencies": {
"@chartwerk/core": "^0.6.26"
},
"devDependencies": {
"copy-webpack-plugin": "^11.0.0",
"css-loader": "^6.8.1",
"style-loader": "^3.3.3",
"ts-loader": "^9.4.3",
"typescript": "^5.1.3",
"webpack": "^5.87.0",
"webpack-cli": "^5.1.4"
},
"packageManager": "yarn@3.2.1",
"workspaces": [
"react/*"
]
"packageManager": "yarn@3.2.1"
}

0
LICENSE → packages/line-pod/LICENSE

10
build/webpack.base.conf.js → packages/line-pod/build/webpack.base.conf.js

@ -1,5 +1,4 @@
const path = require('path');
const CopyPlugin = require("copy-webpack-plugin");
function resolve(dir) {
@ -9,14 +8,7 @@ function resolve(dir) {
module.exports = {
context: resolve('src'),
entry: './index.ts',
plugins: [
new CopyPlugin({
patterns: [
{ from: "../react/dist/index.js", to: "react/index.js" },
{ from: "../react/dist/index.d.ts", to: "react/index.d.ts" },
],
})
],
plugins: [],
module: {
rules: [
{

0
build/webpack.dev.conf.js → packages/line-pod/build/webpack.dev.conf.js

0
build/webpack.prod.conf.js → packages/line-pod/build/webpack.prod.conf.js

0
examples/basic.html → packages/line-pod/examples/demo.html

0
examples/live.html → packages/line-pod/examples/demo_live.html

0
examples/vertical.html → packages/line-pod/examples/demo_vertical.html

33
packages/line-pod/package.json

@ -0,0 +1,33 @@
{
"name": "line-pod",
"version": "0.6.9",
"description": "Chartwerk line chart",
"main": "dist/index.js",
"files": [
"/dist"
],
"scripts": {
"build": "webpack --config build/webpack.prod.conf.js && webpack --config build/webpack.dev.conf.js",
"dev": "webpack --watch --config build/webpack.dev.conf.js",
"test": "echo \"Error: no test specified\" && exit 1",
"update-core": "yarn up @chartwerk/core && yarn up @chartwerk/core@latest"
},
"repository": {
"type": "git",
"url": "https://gitlab.com/chartwerk/line-pod.git"
},
"author": "CorpGlory",
"license": "ISC",
"dependencies": {
"@chartwerk/core": "latest"
},
"devDependencies": {
"css-loader": "^6.8.1",
"style-loader": "^3.3.3",
"ts-loader": "^9.4.3",
"typescript": "^5.1.3",
"webpack": "^5.87.0",
"webpack-cli": "^5.1.4"
},
"packageManager": "yarn@3.2.1"
}

256
src/index.ts → packages/line-pod/src/index.ts

@ -1,11 +1,7 @@
import { ChartwerkPod, VueChartwerkPodMixin, TimeFormat, CrosshairOrientation, BrushOrientation, yAxisOrientation } from '@chartwerk/core';
import { LineTimeSerie, LineOptions, MouseObj, AreaType } from './types';
import { Markers } from './components/markers';
import { Segments } from './components/segments';
import { LineTimeSerie, LineOptions } from './types';
import { LineSeries } from './models/line_series';
import { MarkersConf } from './models/marker';
import { SegmentSerie } from './models/segment';
import * as d3 from 'd3';
import * as _ from 'lodash';
@ -14,91 +10,64 @@ const METRIC_CIRCLE_RADIUS = 1.5;
const CROSSHAIR_CIRCLE_RADIUS = 3;
const CROSSHAIR_BACKGROUND_RAIDUS = 9;
const CROSSHAIR_BACKGROUND_OPACITY = 0.3;
type Generator = d3.Line<[number, number]> | d3.Area<[number, number]>;
class LinePod extends ChartwerkPod<LineTimeSerie, LineOptions> {
private _markersLayer: Markers = null;
private _segmentsLayer: Segments = null;
constructor(
_el: HTMLElement,
_series: LineTimeSerie[] = [],
_options: LineOptions = {},
private _markersConf?: MarkersConf,
private _segmentSeries: SegmentSerie[] = [],
) {
export class LinePod extends ChartwerkPod<LineTimeSerie, LineOptions> {
lineGenerator = null;
areaGenerator = null;
lineGeneratorY1 = null;
areaGeneratorY1 = null;
constructor(_el: HTMLElement, _series: LineTimeSerie[] = [], _options: LineOptions = {}) {
super(_el, _series, _options);
this.series = new LineSeries(_series);
}
override renderMetrics(): void {
renderMetrics(): void {
this.clearAllMetrics();
this.updateCrosshair();
this.updateEvents();
this.initLineGenerator();
this.initAreaGenerator();
if(!this.series.isSeriesAvailable) {
this.renderNoDataPointsMessage();
return;
}
for(const serie of this.series.visibleSeries) {
const generator = this.getRenderGenerator(serie.renderArea, serie.yOrientation);
this._renderMetric(serie, generator);
}
if(!_.isEmpty(this._markersConf)) {
this._markersLayer = new Markers(this.d3Node, this._markersConf, this.state, this.margin);
this._markersLayer.render(this.metricContainer, this.height);
}
if(!_.isEmpty(this._segmentSeries)) {
this._segmentsLayer = new Segments(this._segmentSeries, this.state);
this._segmentsLayer.render(this.metricContainer, this.chartContainer);
this._renderMetric(serie);
}
}
clearAllMetrics(): void {
// TODO: temporary hack before it will be implemented in core.
this.chartContainer.selectAll('.metric-el').remove();
this._markersLayer?.clear();
}
protected updateEvents(): void {
// overlay - core component that is used to handle mouse events
if(!this.overlay) {
return;
}
if(this.options._options.events?.contextMenu === undefined) {
return;
}
this.overlay.on('contextmenu', this.onContextMenu.bind(this));
initLineGenerator(): void {
this.lineGenerator = d3.line()
.x(d => this.state.xScale(d[0]))
.y(d => this.state.yScale(d[1]));
this.lineGeneratorY1 = d3.line()
.x(d => this.state.xScale(d[0]))
.y(d => this.state.y1Scale(d[1]));
}
getRenderGenerator(renderArea: AreaType, yOrientation: yAxisOrientation): Generator {
const yScale = yOrientation === yAxisOrientation.LEFT ? this.state.yScale : this.state.y1Scale;
const yValueRange = yOrientation === yAxisOrientation.LEFT ? this.state.yValueRange : this.state.y1ValueRange;
const yAxisOptions = yOrientation === yAxisOrientation.LEFT ? this.options.axis.y : this.options.axis.y1;
const topChartBorder = !yAxisOptions.invert ? yScale(yValueRange[1]) : yScale(yValueRange[0]);
const bottomChartBorder = !yAxisOptions.invert ? yScale(yValueRange[0]) : yScale(yValueRange[1]);
switch(renderArea) {
case AreaType.NONE:
// return line generator
return d3.line()
.x(d => this.state.xScale(d[0]))
.y(d => yScale(d[1]));
case AreaType.ABOVE:
return d3.area()
.x(d => this.state.xScale(d[0]))
.y0(topChartBorder)
.y1(d => yScale(d[1]));
case AreaType.BELOW:
return d3.area()
.x(d => this.state.xScale(d[0]))
.y0(d => yScale(d[1]))
.y1(bottomChartBorder);
default:
throw new Error(`Unknown type of renderArea: ${renderArea}`);
initAreaGenerator(): void {
this.areaGenerator = d3.area()
.x(d => this.state.xScale(d[0]))
.y1(d => this.state.yScale(d[1]))
.y0(d => this.height);
this.areaGeneratorY1 = d3.area()
.x(d => this.state.xScale(d[0]))
.y1(d => this.state.y1Scale(d[1]))
.y0(d => this.height);
}
getRenderGenerator(renderArea: boolean, yOrientation: yAxisOrientation): any {
if(renderArea) {
return yOrientation === yAxisOrientation.LEFT ? this.areaGenerator : this.areaGeneratorY1;
}
return yOrientation === yAxisOrientation.LEFT ? this.lineGenerator : this.areaGeneratorY1;
}
_renderDots(serie: LineTimeSerie): void {
@ -111,12 +80,12 @@ class LinePod extends ChartwerkPod<LineTimeSerie, LineOptions> {
.attr('r', METRIC_CIRCLE_RADIUS)
.style('pointer-events', 'none')
.attr('cx', d => this.state.xScale(d[0]))
.attr('cy', d => this.state.getYScaleByOrientation(serie.yOrientation)(d[1]));
.attr('cy', d => this.state.yScale(d[1]));
}
_renderLines(serie: LineTimeSerie, generator: Generator): void {
const fillColor = serie.renderArea !== AreaType.NONE ? serie.color : 'none';
const fillOpacity = serie.renderArea !== AreaType.NONE ? 0.5 : 'none';
_renderLines(serie: LineTimeSerie): void {
const fillColor = serie.renderArea ? serie.color : 'none';
const fillOpacity = serie.renderArea ? 0.5 : 'none';
this.metricContainer
.append('path')
@ -129,12 +98,12 @@ class LinePod extends ChartwerkPod<LineTimeSerie, LineOptions> {
.attr('stroke-opacity', 0.7)
.attr('pointer-events', 'none')
.style('stroke-dasharray', serie.dashArray)
.attr('d', generator);
.attr('d', this.getRenderGenerator(serie.renderArea, serie.yOrientation));
}
_renderMetric(serie: LineTimeSerie, generator: Generator): void {
_renderMetric(serie: LineTimeSerie): void {
if(serie.renderLines === true) {
this._renderLines(serie, generator);
this._renderLines(serie);
}
if(serie.renderDots === true) {
@ -178,16 +147,24 @@ class LinePod extends ChartwerkPod<LineTimeSerie, LineOptions> {
.style('pointer-events', 'none');
}
public renderSharedCrosshair(values: { x?: number, y?: number }): void {
this.onMouseOver(); // TODO: refactor to use it once
const eventX = this.state.xScale(values.x);
const eventY = this.state.yScale(values.y);
this.moveCrosshairLine(eventX, eventY);
const datapoints = this.findAndHighlightDatapoints(values.x, values.y);
this.options.callbackSharedCrosshairMove({
datapoints: datapoints,
eventX, eventY
});
}
public hideSharedCrosshair(): void {
this.crosshair.style('display', 'none');
}
// TODO: refactor to make xPosition and yPosition optional
// and trough error if they are provided for wrong orientation
moveCrosshairLine(xPosition: number, yPosition: number): void {
this.crosshair.style('display', null);
switch(this.options.crosshair.orientation) {
case CrosshairOrientation.VERTICAL:
this.crosshair.select('#crosshair-line-x')
@ -292,17 +269,18 @@ class LinePod extends ChartwerkPod<LineTimeSerie, LineOptions> {
return _.max(intervals);
}
getMouseObj(): MouseObj {
onMouseMove(): void {
const eventX = d3.mouse(this.chartContainer.node())[0];
const eventY = d3.mouse(this.chartContainer.node())[1];
const xValue = this.state.xScale.invert(eventX); // mouse x position in xScale
const yValue = this.state.yScale.invert(eventY);
this.moveCrosshairLine(eventX, eventY);
const datapoints = this.findAndHighlightDatapoints(xValue, yValue);
// TODO: is shift key pressed
// TDOO: is shift key pressed
// TODO: need to refactor this object
return {
this.options.callbackMouseMove({
x: d3.event.pageX,
y: d3.event.pageY,
xVal: xValue,
@ -310,40 +288,9 @@ class LinePod extends ChartwerkPod<LineTimeSerie, LineOptions> {
series: datapoints,
chartX: eventX,
chartWidth: this.width
};
}
override onMouseMove(): void {
const obj = this.getMouseObj();
const eventX = d3.mouse(this.chartContainer.node())[0];
const eventY = d3.mouse(this.chartContainer.node())[1];
this.moveCrosshairLine(eventX, eventY);
// TODO: is shift key pressed
// TODO: need to refactor this object
this.options.callbackMouseMove(obj);
}
public renderSharedCrosshair(values: { x?: number, y?: number }): void {
this.showCrosshair();
this.moveCrosshairLine(
values.x ? this.state.xScale(values.x) : 0,
values.y ? this.state.yScale(values.y) : 0
);
const datapoints = this.findAndHighlightDatapoints(values.x, values.y);
this.options.callbackSharedCrosshairMove({
datapoints: datapoints,
eventX: values.x ? this.state.xScale(values.x) : 0,
eventY: values.y ? this.state.yScale(values.y) : 0
});
}
override onMouseClick(): void {
this.options.callbackMouseClick(this.getMouseObj());
}
findAndHighlightDatapoints(xValue: number, yValue: number): { value: [number, number], color: string, label: string }[] {
if(!this.series.isSeriesAvailable) {
return [];
@ -373,26 +320,16 @@ class LinePod extends ChartwerkPod<LineTimeSerie, LineOptions> {
return points;
}
showCrosshair() {
onMouseOver(): void {
this.onMouseMove();
this.crosshair.style('display', null);
this.crosshair.selectAll('.crosshair-circle')
.style('display', null);
}
hideCrosshair() {
this.crosshair.style('display', 'none');
this.crosshair.selectAll('.crosshair-circle')
.style('display', 'none');
}
override onMouseOver(): void {
this.showCrosshair();
this.onMouseMove();
}
override onMouseOut(): void {
this.hideCrosshair();
onMouseOut(): void {
this.options.callbackMouseOut();
this.crosshair.style('display', 'none');
}
isDoubleClickActive(): boolean {
@ -404,14 +341,6 @@ class LinePod extends ChartwerkPod<LineTimeSerie, LineOptions> {
this.onMouseOver();
}
updateMarkers(markersConf: MarkersConf): void {
this._markersConf = markersConf;
}
updateSegments(segments: SegmentSerie[]): void {
this._segmentSeries = segments;
}
// methods below rewrite s, (move more methods here)
protected zoomOut(): void {
if(d3.event.type === 'dblclick' && !this.isDoubleClickActive()) {
@ -453,68 +382,13 @@ class LinePod extends ChartwerkPod<LineTimeSerie, LineOptions> {
x: xAxisMiddleValue,
y: yAxisMiddleValue
}
// TODO: refactor core not to take _options explicitly
if(
this.options._options.events !== undefined &&
this.options._options.events.zoomOut !== undefined
) {
this.options._options.events.zoomOut(
centers,
[this.state.xValueRange, this.state.yValueRange]
);
}
}
protected onContextMenu(): void {
d3.event.preventDefault(); // do not open browser's context menu.
const eventX = d3.mouse(this.chartContainer.node())[0];
const eventY = d3.mouse(this.chartContainer.node())[1];
this.options._options.events.contextMenu({
x: this.state.xScale.invert(eventX),
y: this.state.yScale.invert(eventY),
});
}
// override parent updateData method to provide markers and segments
protected updateLineData(
series?: LineTimeSerie[],
options?: LineOptions,
markersConf?: MarkersConf,
segments?: SegmentSerie[],
shouldRerender = true
): void {
this.updateMarkers(markersConf);
this.updateSegments(segments);
this.updateData(series, options, shouldRerender);
this.options.callbackZoomOut(centers);
}
}
// TODO: it should be moved to VUE folder
// it is used with Vue.component, e.g.: Vue.component('chartwerk-line-pod', VueChartwerkLinePod)
export const VueChartwerkLinePod = {
// alternative to `template: '<div class="chartwerk-line-pod" :id="id" />'`
props: {
markersConf: {
type: Object,
required: false,
default: function() { return {}; }
},
segments: {
type: Array,
required: false,
default: function() { return []; }
},
},
watch: {
markersConf() {
this.renderChart();
},
segments() {
this.renderChart();
},
},
render(createElement) {
return createElement(
'div',
@ -528,10 +402,10 @@ export const VueChartwerkLinePod = {
methods: {
render() {
if(this.pod === undefined) {
this.pod = new LinePod(document.getElementById(this.id), this.series, this.options, this.markersConf, this.segments);
this.pod = new LinePod(document.getElementById(this.id), this.series, this.options);
this.pod.render();
} else {
this.pod.updateLineData(this.series, this.options, this.markersConf, this.segments);
this.pod.updateData(this.series, this.options);
}
},
renderSharedCrosshair(values) {
@ -543,4 +417,4 @@ export const VueChartwerkLinePod = {
}
};
export { LineTimeSerie, LineOptions, TimeFormat, LinePod, AreaType, MarkersConf, SegmentSerie };
export { LineTimeSerie, LineOptions, TimeFormat };

5
src/models/line_series.ts → packages/line-pod/src/models/line_series.ts

@ -1,5 +1,5 @@
import { CoreSeries, yAxisOrientation } from '@chartwerk/core';
import { LineTimeSerie, AreaType } from '../types';
import { LineTimeSerie } from '../types';
import * as _ from 'lodash';
@ -10,11 +10,12 @@ const LINE_SERIE_DEFAULTS = {
renderLines: true,
dashArray: '0',
class: '',
renderArea: AreaType.NONE,
renderArea: false,
yOrientation: yAxisOrientation.LEFT,
};
export class LineSeries extends CoreSeries<LineTimeSerie> {
constructor(series: LineTimeSerie[]) {
super(series, _.clone(LINE_SERIE_DEFAULTS));
}

13
packages/line-pod/src/types.ts

@ -0,0 +1,13 @@
import { Serie, Options } from '@chartwerk/core';
type LineTimeSerieParams = {
maxLength: number,
renderDots: boolean,
renderLines: boolean, // TODO: refactor same as scatter-pod
dashArray: string; // dasharray attr, only for lines
class: string; // option to add custom class to each serie element
renderArea: boolean; // TODO: move to render type
}
export type LineTimeSerie = Serie & Partial<LineTimeSerieParams>;
export type LineOptions = Options;

3
react/tsconfig.json → packages/line-pod/tsconfig.json

@ -17,7 +17,6 @@
"noImplicitUseStrict": false,
"noImplicitAny": false,
"noUnusedLocals": false,
"baseUrl": "./src",
"jsx": "react"
"baseUrl": "./src"
}
}

27
packages/react/README.md

@ -0,0 +1,27 @@
# React + TypeScript + Vite
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
## Expanding the ESLint configuration
If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:
- Configure the top-level `parserOptions` property like this:
```js
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
project: ['./tsconfig.json', './tsconfig.node.json'],
tsconfigRootDir: __dirname,
},
```
- Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked`
- Optionally add `plugin:@typescript-eslint/stylistic-type-checked`
- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list

13
packages/react/index.html

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Line-Pod Demo</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

126
packages/react/lib/components/ChartwerkLinePod.tsx

@ -0,0 +1,126 @@
import type { LineTimeSerie, LineOptions } from 'line-pod';
import { LinePod } from 'line-pod';
// import { AxisRange } from '@chartwerk/core/dist/types';
import { useEffect, useRef, useState, PropsWithChildren, forwardRef } from 'react';
import * as _ from 'lodash';
export type AxisRange = [number, number] | undefined;
export type ChartwerkLinePodProps = {
id: string;
series: LineTimeSerie[];
options: LineOptions;
className?: string;
// TODO: callback types should be exported from chartwerk
onZoomIn?: (ranges: AxisRange[]) => void;
onZoomOut?: (centers: { x: number, y: number }) => void;
onMouseMove?: (event: any) => void;
onMouseOut?: () => void;
onLegendClick?: (idx: number) => void,
onPanning?: (event: { ranges: AxisRange[], d3Event: any }) => void;
onPanningEnd?: (ranges: AxisRange[]) => void;
onContextMenu?: (evt: any) => void;
onSharedCrosshairMove?: (evt: any) => void;
onRenderStart?: () => void,
onRenderEnd?: () => void,
}
export const ChartwerkLinePod = forwardRef<HTMLDivElement, PropsWithChildren<ChartwerkLinePodProps>>((props, ref) => {
const [pod, setPod] = useState<LinePod | 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;
}
console.log('remove chart');
// @ts-ignore
pod.removeEventListeners();
}
}, []);
useEffect(() => {
if (chart === null) {
return;
}
let eventsCallbacks = _.cloneDeep(props.options.eventsCallbacks || {});
if (props.onZoomIn) {
eventsCallbacks.zoomIn = props.onZoomIn;
}
if (props.onZoomOut) {
eventsCallbacks.zoomOut = props.onZoomOut;
}
if (props.onMouseMove) {
eventsCallbacks.mouseMove = props.onMouseMove;
}
if (props.onMouseOut) {
eventsCallbacks.mouseOut = props.onMouseOut;
}
if (props.onLegendClick) {
eventsCallbacks.onLegendClick = props.onLegendClick;
}
if (props.onPanning) {
eventsCallbacks.panning = props.onPanning;
}
if (props.onPanningEnd) {
eventsCallbacks.panningEnd = props.onPanningEnd;
}
if (props.onContextMenu) {
eventsCallbacks.contextMenu = props.onContextMenu;
}
if (props.onSharedCrosshairMove) {
eventsCallbacks.sharedCrosshairMove = props.onSharedCrosshairMove;
}
if (props.onRenderStart) {
eventsCallbacks.renderStart = props.onRenderStart;
}
if (pod === null) {
console.log('create chart');
const newPod = new LinePod(
// @ts-ignore
chart,
props.series,
{
...props.options,
eventsCallbacks,
}
);
setPod(
newPod
);
console.log('initial chart render');
newPod.render();
} else {
console.log('update chart');
pod.updateData(props.series, {
...props.options,
eventsCallbacks,
});
}
}, [chart, pod, props.id, props.series, props.options]);
// TODO: it's a hack to render the LinePod right after the div appears in DOM
setTimeout(() => {
if (hack === null) {
setHack(1);
}
}, 1);
return (
<div ref={ref}>
<div id={props.id} className={props.className} ref={chartRef}></div>
{props.children}
</div>
);
});

1
packages/react/lib/main.ts

@ -0,0 +1 @@
export { ChartwerkLinePod } from './components/ChartwerkLinePod';

36
packages/react/package.json

@ -0,0 +1,36 @@
{
"name": "react-line-pod",
"version": "0.0.0",
"type": "module",
"main": "dist/react-line-pod.js",
"types": "dist/main.d.ts",
"files": [
"dist"
],
"scripts": {
"dev": "vite",
"build": "tsc --p ./tsconfig-build.json && vite build",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview"
},
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@types/lodash": "^4.14.201",
"@types/react": "^18.2.15",
"@types/react-dom": "^18.2.7",
"@typescript-eslint/eslint-plugin": "^6.0.0",
"@typescript-eslint/parser": "^6.0.0",
"@vitejs/plugin-react": "^4.0.3",
"eslint": "^8.45.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.3",
"line-pod": "workspace:^",
"lodash": "^4.17.21",
"typescript": "^5.0.2",
"vite": "^4.4.5",
"vite-plugin-dts": "^3.6.3"
}
}

1
packages/react/public/vite.svg

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

36
packages/react/src/App.tsx

@ -0,0 +1,36 @@
import { ChartwerkLinePod } from '../lib/main';
function App() {
const startTime = 1590590148;
const arrayLength = 20;
const data1 = Array.from({ length: arrayLength }, (el, idx) => [startTime + idx * 10000, Math.floor(Math.random() * 40)]);
const data2 = Array.from({ length: arrayLength }, (el, idx) => [startTime + idx * 10000, Math.floor(Math.random() * 10)]);
const data3 = Array.from({ length: arrayLength }, (el, idx) => [startTime + idx * 10000, Math.floor(Math.random() * 20) + 90]);
const options = {
renderLegend: false, usePanning: false,
axis: {
y: { invert: false, range: [0, 350] },
y1: { isActive: true, range: [0, 10], ticksCount: 8 },
x: { format: 'time' }
},
zoomEvents: {
mouse: { zoom: { isActive: true, orientation: 'horizontal' } },
scroll: { zoom: { isActive: true, orientation: 'horizontal' } }
}
};
const series = [
{ target: 'test1', datapoints: data1, color: 'green', dashArray: '5,3', class: 'first', renderArea: true },
{ target: 'test2', datapoints: data2, color: 'blue', yOrientation: 'right' },
{ target: 'test3', datapoints: data3, color: 'orange' },
];
return (
<>
{/* @ts-ignore */}
<ChartwerkLinePod id="demo" series={series} options={options}></ChartwerkLinePod>
</>
);
}
export default App;

10
packages/react/src/main.tsx

@ -0,0 +1,10 @@
import App from './App.tsx';
import React from 'react';
import ReactDOM from 'react-dom/client';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>
);

1
packages/react/src/vite-env.d.ts vendored

@ -0,0 +1 @@
/// <reference types="vite/client" />

6
packages/react/tsconfig-build.json

@ -0,0 +1,6 @@
{
"extends": "./tsconfig.json",
"include": [
"lib"
]
}

25
packages/react/tsconfig.json

@ -0,0 +1,25 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src", "lib"],
"references": [{ "path": "./tsconfig.node.json" }]
}

10
packages/react/tsconfig.node.json

@ -0,0 +1,10 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}

29
packages/react/vite.config.ts

@ -0,0 +1,29 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import dts from 'vite-plugin-dts';
import { resolve } from 'path';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
react(),
dts({ include: ['lib'] }),
],
optimizeDeps: {
include: ['line-pod'],
},
build: {
copyPublicDir: false,
lib: {
entry: resolve(__dirname, 'lib/main.ts'),
formats: ['es']
},
rollupOptions: {
external: ['react', 'react/jsx-runtime'],
},
commonjsOptions: {
include: [/line-pod/],
},
}
});

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/line-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": "line-pod-react",
"version": "0.0.1",
"description": "React wrapper around line-pod",
"main": "dist/index.js",
"repository": "http://code.corpglory.net/chartwerk/line-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/line-pod": "workspace:*",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"ts-loader": "^9.5.1",
"typescript": "^5.4.3",
"webpack": "^5.87.0"
}
}

74
react/src/index.tsx

@ -1,74 +0,0 @@
import { LineTimeSerie, LineOptions, LinePod } from '@chartwerk/line-pod';
import { MarkersConf } from '@chartwerk/line-pod/dist/models/marker';
import { SegmentSerie } from '@chartwerk/line-pod/dist/models/segment';
import React, { useEffect, useRef, useState } from 'react';
import _ from 'lodash';
export type ChartwerkLinePodProps = {
id?: string;
series: LineTimeSerie[];
options?: LineOptions;
markersConf?: MarkersConf,
segments?: SegmentSerie[],
className?: string;
}
export function ChartwerkLinePod(props: ChartwerkLinePodProps) {
const [pod, setPod] = useState<LinePod | 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 LinePod(
// @ts-ignore
chart,
props.series,
props.options,
props.markersConf,
props.segments
);
setPod(newPod);
newPod.render();
} else {
if(props.markersConf) {
pod.updateMarkers(props.markersConf);
}
if(props.segments) {
pod.updateSegments(props.segments);
}
// 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, props.markersConf, props.segments]);
// TODO: it's a hack to render the LinePod 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 ChartwerkLinePod;

1322
react/yarn.lock

File diff suppressed because it is too large Load Diff

139
src/components/markers.ts

@ -1,139 +0,0 @@
import { MarkerElem, MarkersConf, MarkerSerie } from '../models/marker';
import { LineTimeSerie, LineOptions } from '../types';
import { Margin, PodState } from '@chartwerk/core';
import d3 from 'd3';
export class Markers {
private _layerContainer = null;
private _chartHeight = 0;
constructor(
private _chartContainer: d3.Selection<HTMLElement, unknown, null, undefined>,
private _markerConf: MarkersConf,
private _state: PodState<LineTimeSerie, LineOptions>,
private _margin: Margin,
) { }
clear() {
if(this._layerContainer !== null) {
this._layerContainer.remove();
}
this._chartContainer.selectAll('.marker-content').remove();
}
render(metricContainer: d3.Selection<SVGGElement, unknown, null, undefined>, chartHeight: number) {
this._chartHeight = chartHeight;
this._layerContainer = metricContainer
.append('g')
.attr('class', 'markers-layer');
for(const serie of this._markerConf.series) {
this.renderSerie(serie);
}
}
private _getLinePosition(marker: MarkerElem): number {
return this._state.xScale(marker.x);
}
private _renderCircle(marker: MarkerElem) {
const linePosition = this._getLinePosition(marker);
let circle = this._layerContainer.append('circle')
.attr('class', 'gap-circle')
.attr('stroke', marker.color)
.attr('stroke-width', '2px')
.attr('r', 4)
.attr('cx', linePosition)
.attr('cy', 5)
circle
.attr('pointer-events', 'all')
.style('cursor', 'pointer')
.on('mousemove', () => {
const onMouseMove = this._markerConf.events?.onMouseMove;
if(onMouseMove) {
onMouseMove(marker);
return
}
if(marker.alwaysDisplay) {
return;
}
this._chartContainer
.selectAll(`.marker-content-${marker.x}`)
.style('visibility', 'visible')
.style('z-index', 9999);
})
.on('mouseout', () => {
const onMouseOut = this._markerConf.events?.onMouseOut;
if(onMouseOut) {
onMouseOut()
return
}
if(marker.alwaysDisplay) {
return;
}
this._chartContainer
.selectAll(`.marker-content-${marker.x}`)
.style('visibility', 'hidden')
.style('z-index', 1);
});
}
private _renderLine(marker: MarkerElem) {
const linePosition = this._getLinePosition(marker);
this._layerContainer.append('line')
.attr('class', 'gap-line')
.attr('stroke', marker.color)
.attr('stroke-width', '1px')
.attr('stroke-opacity', '0.3')
.attr('stroke-dasharray', '4')
.attr('x1', linePosition)
.attr('x2', linePosition)
.attr('y1', 0)
// @ts-ignore // TODO: remove ignore but boxParams are protected
.attr('y2', this._state.boxParams.height)
.attr('pointer-events', 'none');
}
private _renderTooltip(marker: MarkerElem) {
if(marker.html === undefined) {
return;
}
const linePosition = this._getLinePosition(marker);
const div = this._chartContainer
.append('div')
.attr('class', `marker-content marker-content-${marker.x}`)
// @ts-ignore // TODO: remove ignore but boxParams are protected
.style('top', `${this._state.boxParams.height - this._chartHeight}px`)
.style('visibility', marker.alwaysDisplay ? 'visible' : 'hidden')
.style('position', 'absolute')
.style('border', '1px solid black')
.style('background-color', 'rgb(33, 37, 41)')
.style('color', 'rgb(255, 255, 255)')
.style('line-height', '1.55')
.style('font-size', '0.875rem')
.style('border-radius', '0.5rem')
.style('padding', 'calc(0.3125rem) 0.625rem')
.style('position', 'absolute')
.style('white-space', 'nowrap')
.style('pointer-events', 'none')
.style('z-index', 1)
.html(marker.html);
// align tooltip: center (we need it to be rendered first)
div.style('left', `${linePosition + this._margin.left - div.node().getBoundingClientRect().width / 2}px`)
}
protected renderSerie(serie: MarkerSerie) {
serie.data.forEach((marker: MarkerElem) => {
this._renderLine(marker);
this._renderCircle(marker);
this._renderTooltip(marker);
});
}
}

73
src/components/segments.ts

@ -1,73 +0,0 @@
import { SegmentSerie, SegmentElement } from "../models/segment";
import { PodState } from "@chartwerk/core";
import { LineTimeSerie, LineOptions } from "../types";
import * as d3 from "d3";
const OPACITY = 0.3;
const OPACITY_SELECT = 0.3;
export class Segments {
// TODO: more semantic name
private _d3Holder = null;
private _metricCon = null;
constructor(
private _series: SegmentSerie[],
private _state: PodState<LineTimeSerie, LineOptions>
) {
}
render(metricContainer: d3.Selection<SVGGElement, unknown, null, undefined>, chartContainer: d3.Selection<SVGGElement, unknown, null, undefined>) {
if(this._d3Holder !== null) {
this._d3Holder.remove();
}
this._d3Holder = metricContainer.append('g').attr('class', 'markers-layer');
for (const s of this._series) {
this.renderSerie(chartContainer, s);
}
}
protected renderSerie(chartContainer: d3.Selection<SVGGElement, unknown, null, undefined>, serie: SegmentSerie) {
// TODO: it's hack with core, need to find a better way
const overlay = chartContainer.select('.overlay');
serie.data.forEach((d) => {
// @ts-ignore
const startPositionX = this._state.xScale(d[0]) as number;
// @ts-ignore
const endPositionX = this._state.xScale(d[1]) as number;
const width = endPositionX - startPositionX // Math.max(endPositionX - startPositionX, MIMIMUM_SEGMENT_WIDTH);
const opacity = serie.opacity || OPACITY;
const opacitySelect = serie.opacitySelect || OPACITY_SELECT;
this._d3Holder.append('rect')
.attr('class', 'segment')
.attr('x', startPositionX)
.attr('y', 0)
.attr('width', width)
// @ts-ignore // TODO: remove ignore but boxParams are protected
.attr('height', this._state.boxParams.height)
.attr('opacity', opacity)
.style('fill', serie.color)
.on('mouseover', function() {
if(serie.select === true) {
d3.select(this).attr('opacity', opacitySelect);
if(serie.onSelect) {
serie.onSelect(d);
}
}
})
.on('mouseout', function(e) {
if(serie.select === true) {
d3.select(this).attr('opacity', opacity);
if(serie.onUnselect) {
serie.onUnselect(d);
}
}
})
.on('mousemove', function(e) {
var event = new MouseEvent('mousemove', d3.event);
overlay.node().dispatchEvent(event)
})
});
}
}

19
src/models/marker.ts

@ -1,19 +0,0 @@
export type MarkerElem = {
x: number;
color: string;
html?: string;
alwaysDisplay?: boolean;
payload?: any;
}
export type MarkerSerie = {
data: MarkerElem[];
}
export type MarkersConf = {
series: MarkerSerie[],
events?: {
onMouseMove?: (el: MarkerElem) => void;
onMouseOut?: () => void;
}
}

11
src/models/segment.ts

@ -1,11 +0,0 @@
export type SegmentElement = [number, number, any?];
export type SegmentSerie = {
color: string;
data: SegmentElement[] // [from, to, payload?] payload is any data for tooltip,
select?: boolean,
opacity?: number,
opacitySelect?: number,
onSelect?: (SegmentElement) => void
onUnselect?: (SegmentElement) => void
}

41
src/types.ts

@ -1,41 +0,0 @@
import { Serie, Options } from '@chartwerk/core';
import { AxisRange } from '@chartwerk/core/dist/types';
type LineTimeSerieParams = {
maxLength: number;
renderDots: boolean;
renderLines: boolean; // TODO: refactor same as scatter-pod
dashArray: string; // dasharray attr, only for lines
class: string; // option to add custom class to each serie element
renderArea: AreaType; // default is none
}
export enum AreaType {
NONE = 'None',
ABOVE = 'Above',
BELOW = 'Below',
}
export type LineTimeSerie = Serie & Partial<LineTimeSerieParams>;
export type LineOptions = Options & {
events? : {
zoomOut?: (centers: {
x: number;
y: number;
}, range: AxisRange[]) => void;
contextMenu?: (position: {
x: number;
y: number;
}) => void;
}
}
export type MouseObj = {
x: number,
y: number,
xVal: number,
yVal: number,
series: { value: [number, number], color: string, label: string }[],
chartX: number,
chartWidth: number
}

23
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",
},
"include": ["src/**/*"]
}

3199
yarn.lock

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