Categories
Visx

Add Radial Line with the Visx Library

Spread the love

Visx is a library that lets us add graphics to our React app easily.

In this article, we’ll look at how to use it to add radial lines into our React app.

Install Required Packages

We have to install a few modules.

To get started, we run:

npm i @visx/curve @visx/gradient @visx/group @visx/mock-data @visx/responsive @visx/scale @visx/shape

to install the packages.

Create the Radial Line

We can create the chart by adding the items provided by the modules.

We use the data from the @visx/mock-data module.

Then we can create the radial line chart by writing:

import React, { useRef, useState, useEffect } from "react";
import { Group } from "@visx/group";
import { LineRadial } from "@visx/shape";
import { scaleTime, scaleLog } from "@visx/scale";
import { curveBasisOpen } from "@visx/curve";
import appleStock from "@visx/mock-data/lib/mocks/appleStock";
import { LinearGradient } from "@visx/gradient";
import { animated, useSpring } from "react-spring";

const green = "#e5fd3d";
export const blue = "#aeeef8";
const darkgreen = "#dff84d";
export const background = "#744cca";
const darkbackground = "#603FA8";
const springConfig = {
  tension: 20
};

// utils
function extent(data, value) {
  const values = data.map(value);
  return [Math.min(...values), Math.max(...values)];
}
// accessors
const date = (d) => new Date(d.date).valueOf();
const close = (d) => d.close;

// scales
const xScale = scaleTime({
  range: [0, Math.PI * 2],
  domain: extent(appleStock, date)
});
const yScale = scaleLog({
  domain: extent(appleStock, close)
});

const angle = (d) => xScale(date(d)) ?? 0;
const radius = (d) => yScale(close(d)) ?? 0;

const firstPoint = appleStock[0];
const lastPoint = appleStock[appleStock.length - 1];

const Example = ({ width, height, animate = true }) => {
  const lineRef = useRef(null);
  const [lineLength, setLineLength] = useState(0);
  const [shouldAnimate, setShouldAnimate] = useState(false);

  const spring = useSpring({
    frame: shouldAnimate ? 0 : 1,
    config: springConfig,
    onRest: () => setShouldAnimate(false)
  });

  const effectDependency = lineRef.current;
  useEffect(() => {
    if (lineRef.current) {
      setLineLength(lineRef.current.getTotalLength());
    }
  }, [effectDependency]);

if (width < 10) return null;
  yScale.range([0, height / 2 - 20]);

const yScaleTicks = yScale.ticks();
  const handlePress = () => setShouldAnimate(true);

  return (
    <>
      {animate && (
        <>
          <button
            type="button"
            onClick={handlePress}
            onTouchStart={handlePress}
          >
            Animate
          </button>
          <br />
        </>
      )}
      <svg
        width={width}
        height={height}
        onClick={() => setShouldAnimate(!shouldAnimate)}
      >
        <LinearGradient from={green} to={blue} id="line-gradient" />
        <rect width={width} height={height} fill={background} rx={14} />
        <Group top={height / 2} left={width / 2}>
          {yScaleTicks.map((tick, i) => (
            <circle
              key={`radial-grid-${i}`}
              r={yScale(tick)}
              stroke={blue}
              strokeWidth={1}
              fill={blue}
              fillOpacity={1 / (i + 1) - (1 / i) * 0.2}
              strokeOpacity={0.2}
            />
          ))}
          {yScaleTicks.map((tick, i) => (
            <text
              key={`radial-grid-${i}`}
              y={-(yScale(tick) ?? 0)}
              dy="-.33em"
              fontSize={8}
              fill={blue}
              textAnchor="middle"
            >
              {tick}
            </text>
          ))}

          <LineRadial angle={angle} radius={radius} curve={curveBasisOpen}>
            {({ path }) => {
              const d = path(appleStock) || "";
              return (
                <>
                  <animated.path
                    d={d}
                    ref={lineRef}
                    strokeWidth={2}
                    strokeOpacity={0.8}
                    strokeLinecap="round"
                    fill="none"
                    stroke={animate ? darkbackground : "url(#line-gradient)"}
                  />
                  {shouldAnimate && (
                    <animated.path
                      d={d}
                      strokeWidth={2}
                      strokeOpacity={0.8}
                      strokeLinecap="round"
                      fill="none"
                      stroke="url(#line-gradient)"
                      strokeDashoffset={spring.frame.interpolate(
                        (v) => v * lineLength
                      )}
                      strokeDasharray={lineLength}
                    />
                  )}
                </>
              );
            }}
          </LineRadial>
          {[firstPoint, lastPoint].map((d, i) => {
            const cx = ((xScale(date(d)) ?? 0) * Math.PI) / 180;
            const cy = -(yScale(close(d)) ?? 0);
            return (
              <circle
                key={`line-cap-${i}`}
                cx={cx}
                cy={cy}
                fill={darkgreen}
                r={3}
              />
            );
          })}
        </Group>
      </svg>
    </>
  );
};

export default function App() {
  return (
    <div className="App">
      <Example width={500} height={300} />
    </div>
  );
}

We set the color for the lines and background color with the green , blue and background variables.

darkBackground is used for the dark background.

The extent function returns an array of number with the min and max values of the data.

date and close are data accessors that return the data for the time and value respectively.

The xScale and yScale values are created from the domain and range .

xScale has both the domain and range so that the radial line can be created.

We also need the angle and radius functions to compute the points for the radial line.

We have the shouldAnimate state to set when to animate the filling of the line.

In the Group component, we have the circle element to create the concentric circles.

The text elements have the text with the values for each ring.

The LineRadial component renders the line from the values returned from the angle and radius function which we pass in as props.

The render prop returns the animated.path component, which is created from the d path string.

The 2nd animated.path component has the filled line which is generated by animating the stroke and strokeDashOffset .

The circle below that is added to the start and end of the line respectively as markers for the ends of the line.

Conclusion

We can add a radial line easily into our React app with the Visx library.

By John Au-Yeung

Web developer specializing in React, Vue, and front end development.

Leave a Reply

Your email address will not be published. Required fields are marked *