Docs
πŸš€ ⏐ Getting Started
Custom Charts

Custom Charts

REAVIZ is extremely flexible and allows you to create custom charts.

Almost all of the chart types allow you to pass JSX elements in the props; this allows you to customize every aspect of each element. Additionally, you can mix charts and their child components together to make combination charts.

Radar Chart Example

The Radar chart was the primary driver behind creating REAVIZ. This chart combines multiple chart types to create a new visualization. Its a great example of how to leverage various chart internal components to create new chart types.

This chart is more than just pretty, it served as a central element for cyber-security analysts to identify trends and threats. For more information on the chart and design itself, checkout: From Dashboard to HUD: How JASK reimagined the security analyst user experience (opens in a new tab). The chart was rewritten and visually tweaked from original design for demonstration purposes.

Breaking it Down

Before we start with the code, let's break this chart down to its core elements.

breakdown

While the chart looks pretty intense, its actually not that crazy. The chart is made up of 3 different chart types: scatter, bar, and area. Additionally there is some decorative elements that are pretty simple svg lines.

Code Tutorial

Now that we understand the general structure of the chart type, we can walk through the code (opens in a new tab) for this chart now.

All of the charts in REAVIZ start with the ChartContainer component. This component deals with measuring sizes and creating the root svg element for us. Below is a minimal example of how to configure the ChartContainer component.

<ChartContainer
  id="radar"
  width={500}
  height={500}
  margins={margins}
  xAxisVisible={false}
  yAxisVisible={false}
  center={true}
>
  {({ chartWidth, chartHeight }) => {
    const rad = Math.min(chartWidth, chartHeight) / 2;
    return (
      <Fragment>
        {renderAxis(chartHeight, chartWidth)}
        {renderOuterLines(rad)}
        {renderSideLines(rad)}
        {renderInnerLines(rad)}
        <RadarBarChart height={chartHeight} width={chartWidth} radius={rad} data={barData} />
        <RadarAreaChart height={chartHeight} width={chartWidth} radius={rad} data={areaData} />
        <RadarScatterPlot radius={rad} data={scatterData} />
        <InnerX />
      </Fragment>
    );
  }}
</ChartContainer>

Inside the ChartContainer component, there is a callback that gives us the chart dimensions. We use this to calculate the radius of the chart and then tee up the RadarBarChart, RadarAreaChart and RadarScatterPlot along with a few visual components for axises and decoration.

Let's take a look at the first component RadarBarChart.

const RadarBarChart = ({
  height,
  width,
  radius,
  data
}) => {
  const { innerRadius, outerRadius } = getRadius(9, 12, radius);
  const { yScale, xScale, data: barData } = buildScale(
    data,
    outerRadius,
    innerRadius,
    false
  );
 
  // The `RadialBarSeries` is a internal component used by the `RadialBarChart`.
  // These components are exposed so you can compose them in ways like this.
  return (
    <RadialBarSeries
      id="radar-bars"
      colorScheme={["#016691"]}
      data={barData}
      xScale={xScale}
      yScale={yScale}
      innerRadius={innerRadius}
      width={width}
      height={height}
      bar={
        <RadialBar
          gradient={false}
          guide={
            <RadialGuideBar
              opacity={0.2}
              fill="#016691"
            />
          }
        />
      }
    />
  );
};

This chart uses the RadialBarSeries component to render the bars. We modify the positions and appearence of those RadialBar components by building a custom scale. The buildScale (opens in a new tab) function takes the data and returns scales and a modified data object that the bar chart can understand. This function is relatively simple and used in all the child chart components:

export const buildScale = (
  initialData = [],
  outerRad = 0,
  innerRad = 0,
  isTime = false,
  xDomain
) => {
  const data = buildShallowChartData(initialData);
 
  // If we are dealing with time, we need to use the `scaleTime`
  // scale provided by d3, otherwise lets use the `scaleBand`.
  let xScale;
  if (isTime) {
    xDomain = (xDomain || extent(data, 'x'));
    xScale = scaleTime().range([0, 2 * Math.PI]).domain(xDomain);
  } else {
    xDomain = (xDomain || uniqueBy(data, d => d.x));
    xScale = scaleBand().range([0, 2 * Math.PI]).domain(xDomain);
  }
 
  const yDomain = getYDomain({ data, scaled: false });
 
  return {
    yScale: getRadialYScale(innerRad, outerRad, yDomain),
    xScale,
    data
  };
};

These charts also use a function called getRadius to determine render positions:

export const getRadius = (startIdx, endIdx, rad) => {
  // Leveraging the d3 `scaleLinear` to determine the radius.
  const scale = scaleLinear().domain([0, ARC_COUNT]).range([INNER_RADIUS, rad]);
  const arcs = scale.ticks(ARC_COUNT);
 
  return {
    innerRadius: scale(arcs[startIdx]),
    outerRadius: scale(arcs[endIdx])
  };
};

These function are mainly leveraging the d3-scale library to build the scales and data all the charts expect.

The next chart in the series is RadarAreaChart.

const RadarAreaChart = ({
  height,
  width,
  radius,
  data
}) => {
  const { innerRadius, outerRadius } = getRadius(6, 8, radius);
  const { yScale, xScale, data: areaData } = buildScale(
    data,
    outerRadius,
    innerRadius,
    true
  );
 
  return (
    <RadialAreaSeries
      id="radar-area"
      data={areaData}
      xScale={xScale}
      yScale={yScale}
      height={height}
      width={width}
      outerRadius={outerRadius}
      innerRadius={innerRadius}
      colorScheme={["#1E5DC8"]}
      line={<RadialLine strokeWidth={1} />}
      area={
        <RadialArea
          gradient={
            <RadialGradient
              stops={[
                <GradientStop key={1} offset="40%" stopOpacity={0.5} />,
                <GradientStop key={2} offset="10%" stopOpacity={0.1} />
              ]}
            />
          }
        />
      }
    />
  );
};

Similar to the RadarBarChart component, this chart uses the RadialAreaSeries which is internally used by RadialAreaChart.

Moving the last chart in this series, we have the RadialScatterSeries and as you can imagine, similar to the other charts we utilize the RadialScatterSeries from the interal RadialScatterPlot component.

const RadarScatterPlot = ({
  radius,
  data
}) => {
  const { innerRadius, outerRadius } = getRadius(4, 4, radius);
  const { yScale, xScale, data: scatterData } = buildScale(
    data,
    outerRadius,
    innerRadius,
    false
  );
  return (
    <RadialScatterSeries
      id="radar-scatter"
      data={scatterData}
      xScale={xScale}
      yScale={yScale}
      point={
        <RadialScatterPoint
          symbol={point => {
            let size = 0;
            if (point.value >= 80) {
              size = 200;
            } else if (point.value >= 50) {
              size = 100;
            } else if (point.value >= 40) {
              size = 50;
            }
 
            const d = (symbol()
              .type(symbolTriangle)
              .size(size))();
 
            return (
              <path
                d={d}
                fill="#CB003E"
                stroke="#EF0954"
                strokeWidth="1"
              />
            );
          }}
        />
      }
    />
  );
};

The RadarScatterPlot uses a custom callback to render a triange symbol ( d3-shape code ). We use the value of the point to determine the size of the symbol.

Wrapping Up

And thats it! This example shows you how you can mix and match various different charts to build truley unique visualizations. You can see all the code here (opens in a new tab).