better tooltips for markers #68

Merged
rozetko merged 2 commits from better-markers-tooltips into main 4 months ago
  1. 19
      examples/markers.html
  2. 12
      examples/markers_select.html
  3. 2
      package.json
  4. 168
      src/components/markers.ts
  5. 5
      src/index.ts
  6. 12
      src/models/marker.ts

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); },

2
package.json

@ -1,6 +1,6 @@
{ {
"name": "@chartwerk/line-pod", "name": "@chartwerk/line-pod",
"version": "0.6.21", "version": "0.7.0",
"description": "Chartwerk line chart", "description": "Chartwerk line chart",
"main": "dist/index.js", "main": "dist/index.js",
"files": [ "files": [

168
src/components/markers.ts

@ -1,59 +1,137 @@
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 { 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>
) { }
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);
this._chartContainer
.append('div')
.attr('class', 'marker-content')
.attr('class', `marker-content-${marker.x}`)
// @ts-ignore // TODO: remove ignore but boxParams are protected
.style('top', `${this._state.boxParams.height - this._chartHeight}px`)
.style('left', `${linePosition + 50}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);
} }
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())
}
}); });
} }
} }

5
src/index.ts

@ -51,8 +51,8 @@ class LinePod extends ChartwerkPod<LineTimeSerie, LineOptions> {
this._renderMetric(serie); this._renderMetric(serie);
} }
if(this._markersConf !== undefined) { if(this._markersConf !== undefined) {
this._markersLayer = new Markers(this._markersConf, this.state); this._markersLayer = new Markers(this.d3Node, this._markersConf, this.state);
this._markersLayer.render(this.metricContainer); this._markersLayer.render(this.metricContainer, this.height);
} }
this._segmentsLayer = new Segments(this._segmentSeries, this.state); this._segmentsLayer = new Segments(this._segmentSeries, this.state);
@ -62,6 +62,7 @@ class LinePod extends ChartwerkPod<LineTimeSerie, LineOptions> {
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 { initLineGenerator(): void {

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 = {

Loading…
Cancel
Save