Compare commits

...

31 Commits

Author SHA1 Message Date
vargburz b3e7f943ad 0.7.8 && render segments if exists 2 months ago
vargburz a8408a748c 0.7.7 && handle context menu 3 months ago
vargburz 6e6fa7dc8e 0.7.6 && update vue with markers and conf 3 months ago
rozetko f67ce313dd 0.7.5 3 months ago
rozetko c1c3b77dac Merge pull request 'area generator: support `invert` option for y/y1 axes' (#70) from area-generator-fix-invert into main 3 months ago
rozetko 53c233b969 area generator: support `invert` option for y/y1 axes 3 months ago
vargburz ebcb3185cd 0.7.4 version 3 months ago
vargburz 6422b49bf0 Merge pull request 'update area render type' (#69) from update-area-type-render into main 3 months ago
vargburz 3f9342b5e1 remove log 3 months ago
vargburz bb46fe79be update area render type 3 months ago
rozetko 0c3b404155 0.7.3 4 months ago
rozetko 85cd952b9f hotfix 4 months ago
rozetko 701feaaf2f 0.7.2 4 months ago
rozetko ebb49adf82 markers: render tooltip in center 4 months ago
rozetko f38829787c 0.7.1 4 months ago
rozetko 6549415feb markers hotfix 4 months ago
rozetko 117c0af469 Merge pull request 'better tooltips for markers' (#68) from better-markers-tooltips into main 4 months ago
rozetko aac8f6f5ac bump version to `0.7.0` 4 months ago
rozetko f4354d7a77 better tooltips for markers 4 months ago
rozetko 529f439ac9 0.6.21 5 months ago
rozetko 121f7a0305 fix renderDots not working for right y-axis 5 months ago
rozetko 7e721ab880 0.6.20 5 months ago
rozetko 3663d3eaaa upd chartwerk core to 0.6.25 5 months ago
Coin de Gamma bd598d6a0b Merge pull request '0.6.19' (#64) from 0.6.19 into main 6 months ago
glitch4347 4620ef7aa9 0.6.19 6 months ago
rozetko 44a4e6ce74 Merge pull request 'segment select' (#63) from segment-select into main 6 months ago
glitch4347 04a01b75a1 opacity param 6 months ago
glitch4347 bf3aab2db1 segment select 6 months ago
Coin de Gamma 348a289f1a Merge pull request '0.6.18' (#61) from 0.6.18 into main 8 months ago
glitch4347 17a2fdf94e 0.6.18 8 months ago
Coin de Gamma dc89f6cbb9 Merge pull request 'basic react component implementatino is sep project' (#59) from better-react-component-#58 into main 8 months ago
  1. 2
      .gitignore
  2. 42
      examples/area.html
  3. 19
      examples/markers.html
  4. 12
      examples/markers_select.html
  5. 33
      examples/right_click.html
  6. 9
      examples/segments.html
  7. 46
      examples/segments_select.html
  8. 4
      package.json
  9. 170
      src/components/markers.ts
  10. 48
      src/components/segments.ts
  11. 154
      src/index.ts
  12. 4
      src/models/line_series.ts
  13. 12
      src/models/marker.ts
  14. 9
      src/models/segment.ts
  15. 18
      src/types.ts
  16. 10
      yarn.lock

2
.gitignore vendored

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

42
examples/area.html

@ -0,0 +1,42 @@
<!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>

19
examples/markers.html

@ -13,9 +13,18 @@
const startTime = 1701790172908; const startTime = 1701790172908;
const timeSerieData = [5, 6, 3, 7, 5, 6, 8, 4, 5, 6, 4, 3, 5, 7, 8] const timeSerieData = [5, 6, 3, 7, 5, 6, 8, 4, 5, 6, 4, 3, 5, 7, 8]
.map((el, idx) => [startTime + idx * 1000, el]); .map((el, idx) => [startTime + idx * 1000, el]);
// TODO: make this one-dimensinal data when implemented const markersData1 = [3, 6, 9].map(el => ({
const markersData1 = [3, 6, 9].map(el => [startTime + el * 1000]); x: startTime + el * 1000,
const markersData2 = [4, 11].map(el => [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 = { let options = {
renderLegend: false, renderLegend: false,
axis: { axis: {
@ -31,8 +40,8 @@
options, options,
{ {
series: [ series: [
{ data: markersData1, color: 'red' }, { data: markersData1 },
{ data: markersData2, color: 'blue' }, { data: markersData2 },
] ]
} }
); );

12
examples/markers_select.html

@ -13,11 +13,11 @@
const startTime = 1701790172908; const startTime = 1701790172908;
const timeSerieData = [5, 6, 3, 7, 5, 6, 8, 4, 5, 6, 4, 3, 5, 7, 8] const timeSerieData = [5, 6, 3, 7, 5, 6, 8, 4, 5, 6, 4, 3, 5, 7, 8]
.map((el, idx) => [startTime + idx * 1000, el]); .map((el, idx) => [startTime + idx * 1000, el]);
// TODO: make this one-dimensinal data when implemented const markersData = [3, 6, 9].map(el => ({
const markersData = [3, 6, 9].map(el => [ x: startTime + el * 1000,
startTime + el * 1000, payload: el,
{ el } color: 'red',
]); }));
let options = { let options = {
renderLegend: false, renderLegend: false,
axis: { axis: {
@ -33,7 +33,7 @@
options, options,
{ {
series: [ series: [
{ data: markersData, color: 'red' }, { data: markersData },
], ],
events: { events: {
onMouseMove: (el) => { console.log(el); }, onMouseMove: (el) => { console.log(el); },

33
examples/right_click.html

@ -0,0 +1,33 @@
<!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>

9
examples/segments.html

@ -13,7 +13,7 @@
const startTime = 1701790172908; const startTime = 1701790172908;
const timeSerieData = [5, 6, 3, 7, 5, 6, 8, 4, 5, 6, 4, 3, 5, 7, 8] const timeSerieData = [5, 6, 3, 7, 5, 6, 8, 4, 5, 6, 4, 3, 5, 7, 8]
.map((el, idx) => [startTime + idx * 1000, el]); .map((el, idx) => [startTime + idx * 1000, el]);
const segmentsData = [3, 6, 9].map(el => [startTime + el * 1000, startTime + (el + 1) * 1000]); const segmentsData = [3, 6, 9].map(el => [startTime + el * 1000, startTime + (el + 1) * 1100]);
let options = { let options = {
renderLegend: false, renderLegend: false,
axis: { axis: {
@ -27,9 +27,12 @@
{ datapoints: timeSerieData, color: 'black' }, { datapoints: timeSerieData, color: 'black' },
], ],
options, options,
[], undefined,
[ [
{ data: segmentsData, color:'#FFE545' } {
data: segmentsData,
color:'#FFE545'
}
] ]
); );
pod.render(); pod.render();

46
examples/segments_select.html

@ -0,0 +1,46 @@
<!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>

4
package.json

@ -1,6 +1,6 @@
{ {
"name": "@chartwerk/line-pod", "name": "@chartwerk/line-pod",
"version": "0.6.17", "version": "0.7.8",
"description": "Chartwerk line chart", "description": "Chartwerk line chart",
"main": "dist/index.js", "main": "dist/index.js",
"files": [ "files": [
@ -19,7 +19,7 @@
"author": "CorpGlory", "author": "CorpGlory",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@chartwerk/core": "^0.6.23" "@chartwerk/core": "^0.6.26"
}, },
"devDependencies": { "devDependencies": {
"copy-webpack-plugin": "^11.0.0", "copy-webpack-plugin": "^11.0.0",

170
src/components/markers.ts

@ -1,59 +1,139 @@
import { MarkersConf, MarkerSerie } from "../models/marker"; import { MarkerElem, MarkersConf, MarkerSerie } from '../models/marker';
import { PodState } from "@chartwerk/core"; import { LineTimeSerie, LineOptions } from '../types';
import { LineTimeSerie, LineOptions } from "../types";
import d3 from "d3"; import { Margin, PodState } from '@chartwerk/core';
import d3 from 'd3';
export class Markers { export class Markers {
// TODO: more semantic name private _layerContainer = null;
private _d3Holder = null; private _chartHeight = 0;
constructor(private _markerConf: MarkersConf, private _state: PodState<LineTimeSerie, LineOptions>) { 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>) { render(metricContainer: d3.Selection<SVGGElement, unknown, null, undefined>, chartHeight: number) {
if(this._d3Holder !== null) { this._chartHeight = chartHeight;
this._d3Holder.remove(); this._layerContainer = metricContainer
.append('g')
.attr('class', 'markers-layer');
for(const serie of this._markerConf.series) {
this.renderSerie(serie);
} }
this._d3Holder = metricContainer.append('g').attr('class', 'markers-layer'); }
for (const ms of this._markerConf.series) {
this.renderSerie(ms); 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) { protected renderSerie(serie: MarkerSerie) {
serie.data.forEach((d) => { serie.data.forEach((marker: MarkerElem) => {
let linePosition = this._state.xScale(d[0]) as number; this._renderLine(marker);
this._d3Holder.append('line') this._renderCircle(marker);
.attr('class', 'gap-line') this._renderTooltip(marker);
.attr('stroke', serie.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');
let circle = this._d3Holder.append('circle')
.attr('class', 'gap-circle')
.attr('stroke', serie.color)
.attr('stroke-width', '2px')
.attr('r', 4)
.attr('cx', linePosition)
.attr('cy', 5)
if(this._markerConf !== undefined) {
circle
.attr('pointer-events', 'all')
.style('cursor', 'pointer')
.on('mousemove', () => this._markerConf.events.onMouseMove(d))
.on('mouseout', () => this._markerConf.events.onMouseOut())
}
}); });
} }
} }

48
src/components/segments.ts

@ -1,34 +1,44 @@
import { SegmentSerie } from "../models/segment"; import { SegmentSerie, SegmentElement } from "../models/segment";
import { PodState } from "@chartwerk/core"; import { PodState } from "@chartwerk/core";
import { LineTimeSerie, LineOptions } from "../types"; import { LineTimeSerie, LineOptions } from "../types";
import d3 from "d3"; import * as d3 from "d3";
const OPACITY = 0.3;
const OPACITY_SELECT = 0.3;
export class Segments { export class Segments {
// TODO: more semantic name // TODO: more semantic name
private _d3Holder = null; private _d3Holder = null;
private _metricCon = null;
constructor(private _series: SegmentSerie[], private _state: PodState<LineTimeSerie, LineOptions>) { constructor(
private _series: SegmentSerie[],
private _state: PodState<LineTimeSerie, LineOptions>
) {
} }
render(metricContainer: d3.Selection<SVGGElement, unknown, null, undefined>) { render(metricContainer: d3.Selection<SVGGElement, unknown, null, undefined>, chartContainer: d3.Selection<SVGGElement, unknown, null, undefined>) {
if(this._d3Holder !== null) { if(this._d3Holder !== null) {
this._d3Holder.remove(); this._d3Holder.remove();
} }
this._d3Holder = metricContainer.append('g').attr('class', 'markers-layer'); this._d3Holder = metricContainer.append('g').attr('class', 'markers-layer');
for (const s of this._series) { for (const s of this._series) {
this.renderSerie(s); this.renderSerie(chartContainer, s);
} }
} }
protected renderSerie(serie: SegmentSerie) { 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) => { serie.data.forEach((d) => {
// @ts-ignore // @ts-ignore
const startPositionX = this._state.xScale(d[0]) as number; const startPositionX = this._state.xScale(d[0]) as number;
// @ts-ignore // @ts-ignore
const endPositionX = this._state.xScale(d[1]) as number; const endPositionX = this._state.xScale(d[1]) as number;
const width = endPositionX - startPositionX // Math.max(endPositionX - startPositionX, MIMIMUM_SEGMENT_WIDTH); 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') this._d3Holder.append('rect')
.attr('class', 'segment') .attr('class', 'segment')
.attr('x', startPositionX) .attr('x', startPositionX)
@ -36,10 +46,28 @@ export class Segments {
.attr('width', width) .attr('width', width)
// @ts-ignore // TODO: remove ignore but boxParams are protected // @ts-ignore // TODO: remove ignore but boxParams are protected
.attr('height', this._state.boxParams.height) .attr('height', this._state.boxParams.height)
.attr('opacity', 0.3) .attr('opacity', opacity)
.style('fill', serie.color) .style('fill', serie.color)
.style('pointer-events', 'none'); .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)
})
}); });
} }
} }

154
src/index.ts

@ -1,5 +1,5 @@
import { ChartwerkPod, VueChartwerkPodMixin, TimeFormat, CrosshairOrientation, BrushOrientation, yAxisOrientation } from '@chartwerk/core'; import { ChartwerkPod, VueChartwerkPodMixin, TimeFormat, CrosshairOrientation, BrushOrientation, yAxisOrientation } from '@chartwerk/core';
import { LineTimeSerie, LineOptions, MouseObj } from './types'; import { LineTimeSerie, LineOptions, MouseObj, AreaType } from './types';
import { Markers } from './components/markers'; import { Markers } from './components/markers';
import { Segments } from './components/segments'; import { Segments } from './components/segments';
@ -14,14 +14,9 @@ const METRIC_CIRCLE_RADIUS = 1.5;
const CROSSHAIR_CIRCLE_RADIUS = 3; const CROSSHAIR_CIRCLE_RADIUS = 3;
const CROSSHAIR_BACKGROUND_RAIDUS = 9; const CROSSHAIR_BACKGROUND_RAIDUS = 9;
const CROSSHAIR_BACKGROUND_OPACITY = 0.3; const CROSSHAIR_BACKGROUND_OPACITY = 0.3;
type Generator = d3.Line<[number, number]> | d3.Area<[number, number]>;
class LinePod extends ChartwerkPod<LineTimeSerie, LineOptions> { class LinePod extends ChartwerkPod<LineTimeSerie, LineOptions> {
lineGenerator = null;
areaGenerator = null;
lineGeneratorY1 = null;
areaGeneratorY1 = null;
private _markersLayer: Markers = null; private _markersLayer: Markers = null;
private _segmentsLayer: Segments = null; private _segmentsLayer: Segments = null;
@ -40,55 +35,70 @@ class LinePod extends ChartwerkPod<LineTimeSerie, LineOptions> {
this.clearAllMetrics(); this.clearAllMetrics();
this.updateCrosshair(); this.updateCrosshair();
this.initLineGenerator(); this.updateEvents();
this.initAreaGenerator();
if(!this.series.isSeriesAvailable) { if(!this.series.isSeriesAvailable) {
this.renderNoDataPointsMessage(); this.renderNoDataPointsMessage();
return; return;
} }
for(const serie of this.series.visibleSeries) { for(const serie of this.series.visibleSeries) {
this._renderMetric(serie); const generator = this.getRenderGenerator(serie.renderArea, serie.yOrientation);
this._renderMetric(serie, generator);
} }
if(this._markersConf !== undefined) { if(!_.isEmpty(this._markersConf)) {
this._markersLayer = new Markers(this._markersConf, this.state); this._markersLayer = new Markers(this.d3Node, this._markersConf, this.state, this.margin);
this._markersLayer.render(this.metricContainer); this._markersLayer.render(this.metricContainer, this.height);
} }
this._segmentsLayer = new Segments(this._segmentSeries, this.state); if(!_.isEmpty(this._segmentSeries)) {
this._segmentsLayer.render(this.metricContainer); this._segmentsLayer = new Segments(this._segmentSeries, this.state);
this._segmentsLayer.render(this.metricContainer, this.chartContainer);
}
} }
clearAllMetrics(): void { clearAllMetrics(): void {
// TODO: temporary hack before it will be implemented in core. // TODO: temporary hack before it will be implemented in core.
this.chartContainer.selectAll('.metric-el').remove(); this.chartContainer.selectAll('.metric-el').remove();
this._markersLayer?.clear();
} }
initLineGenerator(): void { protected updateEvents(): void {
this.lineGenerator = d3.line() // overlay - core component that is used to handle mouse events
.x(d => this.state.xScale(d[0])) if(!this.overlay) {
.y(d => this.state.yScale(d[1])); return;
this.lineGeneratorY1 = d3.line() }
.x(d => this.state.xScale(d[0])) if(this.options._options.events?.contextMenu === undefined) {
.y(d => this.state.y1Scale(d[1])); return;
} }
this.overlay.on('contextmenu', this.onContextMenu.bind(this));
initAreaGenerator(): void { }
this.areaGenerator = d3.area()
.x(d => this.state.xScale(d[0])) getRenderGenerator(renderArea: AreaType, yOrientation: yAxisOrientation): Generator {
.y1(d => this.state.yScale(d[1])) const yScale = yOrientation === yAxisOrientation.LEFT ? this.state.yScale : this.state.y1Scale;
.y0(d => this.height); const yValueRange = yOrientation === yAxisOrientation.LEFT ? this.state.yValueRange : this.state.y1ValueRange;
this.areaGeneratorY1 = d3.area() const yAxisOptions = yOrientation === yAxisOrientation.LEFT ? this.options.axis.y : this.options.axis.y1;
.x(d => this.state.xScale(d[0]))
.y1(d => this.state.y1Scale(d[1])) const topChartBorder = !yAxisOptions.invert ? yScale(yValueRange[1]) : yScale(yValueRange[0]);
.y0(d => this.height); const bottomChartBorder = !yAxisOptions.invert ? yScale(yValueRange[0]) : yScale(yValueRange[1]);
} switch(renderArea) {
case AreaType.NONE:
getRenderGenerator(renderArea: boolean, yOrientation: yAxisOrientation): any { // return line generator
if(renderArea) { return d3.line()
return yOrientation === yAxisOrientation.LEFT ? this.areaGenerator : this.areaGeneratorY1; .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}`);
} }
return yOrientation === yAxisOrientation.LEFT ? this.lineGenerator : this.areaGeneratorY1;
} }
_renderDots(serie: LineTimeSerie): void { _renderDots(serie: LineTimeSerie): void {
@ -101,12 +111,12 @@ class LinePod extends ChartwerkPod<LineTimeSerie, LineOptions> {
.attr('r', METRIC_CIRCLE_RADIUS) .attr('r', METRIC_CIRCLE_RADIUS)
.style('pointer-events', 'none') .style('pointer-events', 'none')
.attr('cx', d => this.state.xScale(d[0])) .attr('cx', d => this.state.xScale(d[0]))
.attr('cy', d => this.state.yScale(d[1])); .attr('cy', d => this.state.getYScaleByOrientation(serie.yOrientation)(d[1]));
} }
_renderLines(serie: LineTimeSerie): void { _renderLines(serie: LineTimeSerie, generator: Generator): void {
const fillColor = serie.renderArea ? serie.color : 'none'; const fillColor = serie.renderArea !== AreaType.NONE ? serie.color : 'none';
const fillOpacity = serie.renderArea ? 0.5 : 'none'; const fillOpacity = serie.renderArea !== AreaType.NONE ? 0.5 : 'none';
this.metricContainer this.metricContainer
.append('path') .append('path')
@ -119,12 +129,12 @@ class LinePod extends ChartwerkPod<LineTimeSerie, LineOptions> {
.attr('stroke-opacity', 0.7) .attr('stroke-opacity', 0.7)
.attr('pointer-events', 'none') .attr('pointer-events', 'none')
.style('stroke-dasharray', serie.dashArray) .style('stroke-dasharray', serie.dashArray)
.attr('d', this.getRenderGenerator(serie.renderArea, serie.yOrientation)); .attr('d', generator);
} }
_renderMetric(serie: LineTimeSerie): void { _renderMetric(serie: LineTimeSerie, generator: Generator): void {
if(serie.renderLines === true) { if(serie.renderLines === true) {
this._renderLines(serie); this._renderLines(serie, generator);
} }
if(serie.renderDots === true) { if(serie.renderDots === true) {
@ -446,7 +456,7 @@ class LinePod extends ChartwerkPod<LineTimeSerie, LineOptions> {
// TODO: refactor core not to take _options explicitly // TODO: refactor core not to take _options explicitly
if( if(
this.options._options.events !== undefined && this.options._options.events !== undefined &&
this.options._options.events.zoomOut !== undefined this.options._options.events.zoomOut !== undefined
) { ) {
this.options._options.events.zoomOut( this.options._options.events.zoomOut(
@ -455,12 +465,56 @@ class LinePod extends ChartwerkPod<LineTimeSerie, LineOptions> {
); );
} }
} }
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);
}
} }
// TODO: it should be moved to VUE folder // TODO: it should be moved to VUE folder
// it is used with Vue.component, e.g.: Vue.component('chartwerk-line-pod', VueChartwerkLinePod) // it is used with Vue.component, e.g.: Vue.component('chartwerk-line-pod', VueChartwerkLinePod)
export const VueChartwerkLinePod = { export const VueChartwerkLinePod = {
// alternative to `template: '<div class="chartwerk-line-pod" :id="id" />'` // 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) { render(createElement) {
return createElement( return createElement(
'div', 'div',
@ -474,10 +528,10 @@ export const VueChartwerkLinePod = {
methods: { methods: {
render() { render() {
if(this.pod === undefined) { if(this.pod === undefined) {
this.pod = new LinePod(document.getElementById(this.id), this.series, this.options); this.pod = new LinePod(document.getElementById(this.id), this.series, this.options, this.markersConf, this.segments);
this.pod.render(); this.pod.render();
} else { } else {
this.pod.updateData(this.series, this.options); this.pod.updateLineData(this.series, this.options, this.markersConf, this.segments);
} }
}, },
renderSharedCrosshair(values) { renderSharedCrosshair(values) {
@ -489,4 +543,4 @@ export const VueChartwerkLinePod = {
} }
}; };
export { LineTimeSerie, LineOptions, TimeFormat, LinePod }; export { LineTimeSerie, LineOptions, TimeFormat, LinePod, AreaType, MarkersConf, SegmentSerie };

4
src/models/line_series.ts

@ -1,5 +1,5 @@
import { CoreSeries, yAxisOrientation } from '@chartwerk/core'; import { CoreSeries, yAxisOrientation } from '@chartwerk/core';
import { LineTimeSerie } from '../types'; import { LineTimeSerie, AreaType } from '../types';
import * as _ from 'lodash'; import * as _ from 'lodash';
@ -10,7 +10,7 @@ const LINE_SERIE_DEFAULTS = {
renderLines: true, renderLines: true,
dashArray: '0', dashArray: '0',
class: '', class: '',
renderArea: false, renderArea: AreaType.NONE,
yOrientation: yAxisOrientation.LEFT, yOrientation: yAxisOrientation.LEFT,
}; };

12
src/models/marker.ts

@ -1,9 +1,13 @@
export type MarkerElem = [number, any?]; export type MarkerElem = {
x: number;
color: string;
html?: string;
alwaysDisplay?: boolean;
payload?: any;
}
export type MarkerSerie = { export type MarkerSerie = {
color: string; data: MarkerElem[];
// TODO: make one-dimensional array with only x
data: MarkerElem[] // [x, payload] payload is any data for tooltip
} }
export type MarkersConf = { export type MarkersConf = {

9
src/models/segment.ts

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

18
src/types.ts

@ -2,12 +2,18 @@ import { Serie, Options } from '@chartwerk/core';
import { AxisRange } from '@chartwerk/core/dist/types'; import { AxisRange } from '@chartwerk/core/dist/types';
type LineTimeSerieParams = { type LineTimeSerieParams = {
maxLength: number, maxLength: number;
renderDots: boolean, renderDots: boolean;
renderLines: boolean, // TODO: refactor same as scatter-pod renderLines: boolean; // TODO: refactor same as scatter-pod
dashArray: string; // dasharray attr, only for lines dashArray: string; // dasharray attr, only for lines
class: string; // option to add custom class to each serie element class: string; // option to add custom class to each serie element
renderArea: boolean; // TODO: move to render type renderArea: AreaType; // default is none
}
export enum AreaType {
NONE = 'None',
ABOVE = 'Above',
BELOW = 'Below',
} }
export type LineTimeSerie = Serie & Partial<LineTimeSerieParams>; export type LineTimeSerie = Serie & Partial<LineTimeSerieParams>;
@ -17,6 +23,10 @@ export type LineOptions = Options & {
x: number; x: number;
y: number; y: number;
}, range: AxisRange[]) => void; }, range: AxisRange[]) => void;
contextMenu?: (position: {
x: number;
y: number;
}) => void;
} }
} }

10
yarn.lock

@ -5,13 +5,13 @@ __metadata:
version: 6 version: 6
cacheKey: 8 cacheKey: 8
"@chartwerk/core@npm:^0.6.23": "@chartwerk/core@npm:^0.6.26":
version: 0.6.23 version: 0.6.26
resolution: "@chartwerk/core@npm:0.6.23" resolution: "@chartwerk/core@npm:0.6.26"
dependencies: dependencies:
d3: ^5.16.0 d3: ^5.16.0
lodash: ^4.17.21 lodash: ^4.17.21
checksum: 629b0438e8cea02914e12956069d318caa98e6b3e2dd2514aab267474fa87e0aa92c190c4ca0fe95ca8091f83be1e1897801f5632c3f11d9cb3be39fa89cca84 checksum: d77ef83701dc13cf2b7fb36dc96448060b6301928bcc0730a7150930f83c51f295e176bcda4e1b8cb8f56d15fef5696edfe6f4e1033adbb5ef5d3487a02c3390
languageName: node languageName: node
linkType: hard linkType: hard
@ -19,7 +19,7 @@ __metadata:
version: 0.0.0-use.local version: 0.0.0-use.local
resolution: "@chartwerk/line-pod@workspace:." resolution: "@chartwerk/line-pod@workspace:."
dependencies: dependencies:
"@chartwerk/core": ^0.6.23 "@chartwerk/core": ^0.6.26
copy-webpack-plugin: ^11.0.0 copy-webpack-plugin: ^11.0.0
css-loader: ^6.8.1 css-loader: ^6.8.1
style-loader: ^3.3.3 style-loader: ^3.3.3

Loading…
Cancel
Save