Skip to content
Snippets Groups Projects
Donut.tsx 5.72 KiB
Newer Older
Tarje.Lavik's avatar
Tarje.Lavik committed
/* eslint-disable no-unused-vars */
import React, { useState } from 'react'
import Pie, { ProvidedProps, PieArcDatum } from '@visx/shape/lib/shapes/Pie'
import { scaleOrdinal } from '@visx/scale'
import { Group } from '@visx/group'

import { animated, useTransition, interpolate } from 'react-spring'

type Slice = {
  id: string
  value: number
}

/* const test: Slice[] = [
  {
    id: 'Universitetet i Bergen',
    value: 67,
  },
  {
    id: 'Havforskningsinstituttet',
    value: 59,
  },
  {
    id: 'Alfred Wegner Institute - Helmholz Centre for Polar Marine Research / Helmholz Association',
    value: 57,
  },
  {
    id: 'Norges Arktiske Universitet, UiT',
    value: 51,
  },
  {
    id: 'Bjerknessenteret for klimaforskning',
    value: 48,
  },
  {
    id: 'Norsk Polarinstitutt',
    value: 40,
  },
  {
    id: 'Russian Academy of Sciences',
    value: 37,
  },
  {
    id: 'Universitetet I Oslo',
    value: 36,
  },
  {
    id: 'UNIS - The University Centre in Svalbard',
    value: 26,
  },
  {
    id: 'University of Alaska',
    value: 26,
  },
  {
    id: 'Woods Hole Oceanographic Institution',
    value: 26,
  },
  {
    id: 'Centre National de la Recherche Scientifique, CNRS',
    value: 21,
  },
] */

const defaultMargin = { top: 20, right: 20, bottom: 20, left: 20 }

export type PieProps = {
  width: number
  height: number
  margin?: typeof defaultMargin
  animate?: boolean
  data: Slice[]
}

export default function Donut({
  width,
  height,
  margin = defaultMargin,
  animate = true,
  data,
}: PieProps) {
  const [selectedSlice, setSelectedSlice] = useState<string | null>(null)
  const test: Slice[] = data
  // accessor functions
  const n = (d: Slice) => d.value

  // color scales
  const getSliceColor = scaleOrdinal({
    domain: [...test.flatMap((s) => s.id)],
    range: [
      'rgba(255,255,255,0.7)',
      'rgba(255,255,255,0.6)',
      'rgba(255,250,255,0.5)',
      'rgba(255,255,255,0.4)',
      'rgba(255,255,255,0.3)',
      'rgba(255,255,255,0.2)',
      'rgba(255,255,255,0.1)',
    ],
  })

  if (width < 10) return null

  const innerWidth = width - margin.left - margin.right
  const innerHeight = height - margin.top - margin.bottom
  const radius = Math.min(innerWidth, innerHeight) / 2
  const centerY = innerHeight / 2
  const centerX = innerWidth / 2
  const donutThickness = 200

  /* console.log(JSON.stringify(test, null, 2), getSliceColor('Universitetet i Bergen')) */

  return (
    <svg width={width} height={height}>
      <rect rx={14} width={width} height={height} />
      <Group top={centerY + margin.top} left={centerX + margin.left}>
        <Pie
          data={selectedSlice ? test.filter(({ id }) => id === selectedSlice) : test}
          pieValue={n}
          outerRadius={radius}
          innerRadius={radius - donutThickness}
          cornerRadius={3}
          padAngle={0.005}
        >
          {(pie) => (
            <AnimatedPie<Slice>
              {...pie}
              animate={animate}
              getKey={(arc) => arc.data.id.slice(0, 40) + '...'}
              onClickDatum={({ data: { id } }) =>
                animate && setSelectedSlice(selectedSlice && selectedSlice === id ? null : id)
              }
              getColor={(arc) => getSliceColor(arc.data.id)}
            />
          )}
        </Pie>
      </Group>
      {animate && (
        <text
          textAnchor="end"
          x={width - 16}
          y={height - 16}
          fill="white"
          fontSize={11}
          fontWeight={300}
          pointerEvents="none"
        >
          Click segments to update
        </text>
      )}
    </svg>
  )
}

// react-spring transition definitions
type AnimatedStyles = { startAngle: number; endAngle: number; opacity: number }

const fromLeaveTransition = ({ endAngle }: PieArcDatum<any>) => ({
  // enter from 360° if end angle is > 180°
  startAngle: endAngle > Math.PI ? 2 * Math.PI : 0,
  endAngle: endAngle > Math.PI ? 2 * Math.PI : 0,
  opacity: 0,
})
const enterUpdateTransition = ({ startAngle, endAngle }: PieArcDatum<any>) => ({
  startAngle,
  endAngle,
  opacity: 1,
})

type AnimatedPieProps<Datum> = ProvidedProps<Datum> & {
  animate?: boolean
  getKey: (d: PieArcDatum<Datum>) => string
  getColor: (d: PieArcDatum<Datum>) => string
  onClickDatum: (d: PieArcDatum<Datum>) => void
  delay?: number
}

function AnimatedPie<Datum>({
  animate,
  arcs,
  path,
  getKey,
  getColor,
  onClickDatum,
}: AnimatedPieProps<Datum>) {
  const transitions = useTransition<PieArcDatum<Datum>, AnimatedStyles>(arcs, {
    from: animate ? fromLeaveTransition : enterUpdateTransition,
    enter: enterUpdateTransition,
    update: enterUpdateTransition,
    leave: animate ? fromLeaveTransition : enterUpdateTransition,
    keys: getKey,
  })
  return transitions((props, arc, { key }) => {
    const [centroidX, centroidY] = path.centroid(arc)
    const hasSpaceForLabel = arc.endAngle - arc.startAngle >= 0.1

    return (
      <g key={key}>
        <animated.path
          // compute interpolated path d attribute from intermediate angle values
          d={interpolate([props.startAngle, props.endAngle], (startAngle, endAngle) =>
            path({
              ...arc,
              startAngle,
              endAngle,
            }),
          )}
          fill={getColor(arc)}
          onClick={() => onClickDatum(arc)}
          onTouchStart={() => onClickDatum(arc)}
        />
        {hasSpaceForLabel && (
          <animated.g style={{ opacity: props.opacity }}>
            <text
              fill="white"
              x={centroidX}
              y={centroidY}
              dy=".33em"
              fontSize={9}
              textAnchor="middle"
              pointerEvents="none"
            >
              {getKey(arc)}
            </text>
          </animated.g>
        )}
      </g>
    )
  })
}