import { getClosestItemIndexes, getStrokeDasharrayCanvas, strokeDashTypes, getAxisCanvas, getMouseCanvas, GenericChartComponent, MoreProps, } from "@fishbot/chart-core"; import { line, CurveFactoryLineOnly, CurveFactory } from "d3-shape"; import * as React from "react"; export interface LineSeriesProps { readonly canvasClip?: (context: CanvasRenderingContext2D, moreProps: any) => void; /** * Wether to connect the line between undefined data points. */ readonly connectNulls?: boolean; /** * A factory for a curve generator for the line. */ readonly curve?: CurveFactory | CurveFactoryLineOnly; /** * Function to decide if a data point has been defined. */ readonly defined?: (d: number | undefined) => boolean; /** * Whether to highlight the line when within the `hoverTolerance`. */ readonly highlightOnHover?: boolean; /** * Width to increase the line to on hover. */ readonly hoverStrokeWidth?: number; /** * The distance between the cursor and the closest point in the line. */ readonly hoverTolerance?: number; /** * Click handler. */ readonly onClick?: (e: React.MouseEvent, moreProps: any) => void; /** * Double click handler. */ readonly onDoubleClick?: (e: React.MouseEvent, moreProps: any) => void; /** * Hover handler. */ readonly onHover?: (e: React.MouseEvent, moreProps: any) => void; /** * Unhover handler. */ readonly onUnHover?: (e: React.MouseEvent, moreProps: any) => void; /** * Context menu handler. */ readonly onContextMenu?: (e: React.MouseEvent, moreProps: any) => void; /** * Color, gradient, or pattern to use for the stroke. */ readonly strokeStyle?: string; /** * Stroke dash. */ readonly strokeDasharray?: strokeDashTypes; /** * Stroke width. */ readonly strokeWidth?: number; /** * Selector for data to plot. */ readonly yAccessor: (data: any) => number | undefined; } /** * `LineSeries` component. */ export class LineSeries extends React.Component { public static defaultProps = { connectNulls: false, defined: (d: number | undefined) => d !== undefined && !isNaN(d), hoverStrokeWidth: 4, hoverTolerance: 6, highlightOnHover: false, strokeDasharray: "Solid", strokeStyle: "#2196f3", strokeWidth: 1, }; public render() { const { highlightOnHover, onClick, onContextMenu, onDoubleClick, onHover, onUnHover, strokeDasharray, } = this.props; const hoverProps = highlightOnHover || onHover || onUnHover ? { isHover: this.isHover, drawOn: ["mousemove", "pan"], canvasToDraw: getMouseCanvas, } : { drawOn: ["pan"], canvasToDraw: getAxisCanvas, }; const lineDash = getStrokeDasharrayCanvas(strokeDasharray); return ( ); } private readonly drawOnCanvas = (lineDash?: number[]) => (ctx: CanvasRenderingContext2D, moreProps: MoreProps) => { const { connectNulls, yAccessor, hoverStrokeWidth = LineSeries.defaultProps.hoverStrokeWidth, defined = LineSeries.defaultProps.defined, curve, canvasClip, strokeStyle, strokeWidth = LineSeries.defaultProps.strokeWidth, } = this.props; const { xAccessor, xScale, chartConfig, plotData, hovering } = moreProps; if (!chartConfig) { console.warn("LineSeries received no chartConfig, is it inside of a Chart?"); return; } const { yScale } = chartConfig; if (canvasClip !== undefined) { ctx.save(); canvasClip(ctx, moreProps); } ctx.lineWidth = hovering ? hoverStrokeWidth : strokeWidth; if (strokeStyle !== undefined) { ctx.strokeStyle = strokeStyle; } if (lineDash !== undefined) { ctx.setLineDash(lineDash); } const dataSeries = line() .x((d) => Math.round(xScale(xAccessor(d)))) .y((d) => Math.round(yScale(yAccessor(d)!))); if (curve !== undefined) { dataSeries.curve(curve); } if (!connectNulls) { dataSeries.defined((d) => defined(yAccessor(d))); } ctx.beginPath(); dataSeries.context(ctx)(plotData); ctx.stroke(); if (canvasClip !== undefined) { ctx.restore(); } }; private readonly isHover = (moreProps: any) => { const { highlightOnHover, yAccessor, hoverTolerance = LineSeries.defaultProps.hoverTolerance } = this.props; if (!highlightOnHover) { return false; } const { chartConfig: { yScale, origin }, xAccessor, mouseXY, currentItem, xScale, plotData, } = moreProps; const [x, y] = mouseXY; const radius = hoverTolerance; const { left, right } = getClosestItemIndexes(plotData, xScale.invert(x), xAccessor); if (left === right) { const cy = yScale(yAccessor(currentItem)) + origin[1]; const cx = xScale(xAccessor(currentItem)) + origin[0]; const hovering1 = Math.pow(x - cx, 2) + Math.pow(y - cy, 2) < Math.pow(radius, 2); return hovering1; } else { const l = plotData[left]; const r = plotData[right]; const x1 = xScale(xAccessor(l)) + origin[0]; const y1 = yScale(yAccessor(l)) + origin[1]; const x2 = xScale(xAccessor(r)) + origin[0]; const y2 = yScale(yAccessor(r)) + origin[1]; // y = m * x + b const m /* slope */ = (y2 - y1) / (x2 - x1); const b /* y intercept */ = -1 * m * x1 + y1; const desiredY = Math.round(m * x + b); const hovering2 = y >= desiredY - radius && y <= desiredY + radius; return hovering2; } }; }