netzwerg / react-svg-timeline

React event timeline component based on SVG
Other
93 stars 26 forks source link

Nivo charting example #90

Open kovasap opened 2 years ago

kovasap commented 2 years ago

I'm interested in accomplishing a timeline that mixes spans and points (as this tool displays events in the quickstart) with line charts like in the screenshot-nivo-layer.png example. I'm a bit confused as to how to make this happen - does the screenshot suggest that nivo charts can be embedded in the react-svg-timeline? Or does it instead mean some logic from this tool can be embedded in a nivo chart?

If there is an example that demonstrates how these tools interact somewhere that could be linked in the README, that would be awesome (the example code the generated the screenshot-nivo-layer.png would be perfect)!

netzwerg commented 2 years ago

Unfortunately, I can't share a complete / self-contained example. Here's our InteractionLayer component – maybe it can serve as inspiration:

import React from 'react'
import { CustomLayerProps } from '@nivo/line'
import { useValueFormatter } from '@nivo/core'
import { scaleLinear } from 'd3-scale'
import { Domain, InteractionMode } from 'react-svg-timeline'
import { InteractionHandling, MouseCursor, zoomScaleWidth, MouseAwareSvg, SvgCoordinates } from 'react-svg-timeline'
import { useZoom } from './useZoom'
import { TimelineOnCursorMoveFn } from '../../Timeline'

export interface InteractionLayerProps extends CustomLayerProps {
  maxDomain?: Domain
  domain: Domain
  onZoom: ZoomFn
  onCursorMove: TimelineOnCursorMoveFn
  onInteractionModeChange: InteractionModeChangeFn
}

export type ZoomFn = (domain: Domain) => void

export type InteractionModeChangeFn = (interactionMode: InteractionMode) => void

export const InteractionLayer = ({
  innerHeight,
  innerWidth,
  onZoom,
  onCursorMove,
  onInteractionModeChange,
  domain,
  maxDomain,
  xFormat,
}: InteractionLayerProps) => {
  const [onZoomIn, onZoomOut, onZoomReset, isZoomInPossible, isZoomOutPossible, smallerZoomScale] = useZoom(
    onZoom,
    domain,
    maxDomain
  )

  const timeScale = scaleLinear().domain(domain).range([0, innerWidth])

  const zoomWidth = zoomScaleWidth(smallerZoomScale)

  const onZoomInCustom = (mouseStartX: number, mouseEndX: number) => {
    onZoom([timeScale.invert(mouseStartX), timeScale.invert(mouseEndX)])
  }

  const onPan = (pixelDelta: number) => {
    const [domainMin, domainMax] = domain
    const [rangeMin, rangeMax] = timeScale.range()
    const domainDelta = (pixelDelta / (rangeMax - rangeMin)) * (domainMax - domainMin)
    const [newDomainMin, newDomainMax] = [domainMin + domainDelta, domainMax + domainDelta]
    if (newDomainMax < Date.now()) {
      onZoom([newDomainMin, newDomainMax])
    }
  }

  const valueFormatter = useValueFormatter(xFormat)

  return (
    <MouseAwareSvg width={innerWidth} height={innerHeight}>
      {(mousePosition: SvgCoordinates) => {
        const timeAtCursor = timeScale.invert(mousePosition.x)

        return (
          <InteractionHandling
            width={innerWidth}
            height={innerHeight}
            mousePosition={mousePosition}
            isAnimationInProgress={false}
            isZoomInPossible={isZoomInPossible}
            isZoomOutPossible={isZoomOutPossible}
            isTrimming={false}
            onHover={onCursorMove}
            onZoomIn={() => {
              onZoomIn(timeAtCursor)
            }}
            onZoomOut={() => {
              onZoomOut(timeAtCursor)
            }}
            onZoomInCustom={onZoomInCustom}
            onZoomInCustomInProgress={() => {}}
            onZoomReset={onZoomReset}
            onPan={onPan}
            onTrimStart={() => {}}
            onTrimEnd={() => {}}
            onInteractionModeChange={onInteractionModeChange}
            onInteractionEnd={() => {}}
          >
            {(cursor, interactionMode, setTrimHoverMode) => {
              return (
                <MouseCursor
                  mousePosition={mousePosition.x}
                  cursorLabel={valueFormatter(timeAtCursor)}
                  cursor={cursor}
                  interactionMode={interactionMode}
                  zoomRangeStart={timeScale(timeAtCursor - zoomWidth / 2)!}
                  zoomRangeEnd={timeScale(timeAtCursor + zoomWidth / 2)!}
                  zoomScale={smallerZoomScale}
                  isZoomInPossible={isZoomInPossible}
                />
              )
            }}
          </InteractionHandling>
        )
      }}
    </MouseAwareSvg>
  )
}

Once you have such a component, you can pass it to the layers prop of Nivo's Line chart:

<Line
   ...
   layers={[
             'grid',
             'axes',
             'lines',
             interactionLayer,
             'legends',
     ]}
     ...>

Alternatively, you can flip things around: As of v0.20.0 react-svg-timeline supports custom layers. So you could paint your own line chart onto a layer and hook it in. A simple example can be found here: https://github.com/netzwerg/react-svg-timeline-demo/blob/ffb1a688422c711172d51677c60644ec97711f4a/src/app/components/Main.tsx#L127.