Introducing VictoryHistogram

27 May 2020

We've recently released VictoryHistogram, a new charting component we've added to our Victory library that allows you to create histogram charts in React. Histograms, while looking similar to bar charts, are different. Rather than displaying categorical data, histograms are used to visualize the distribution of data, by "binning" or "bucketing" data points into ranges.

Previously there wasn't an automatic way to do this in Victory; you'd have to do much of the data processing yourself and plot the results with VictoryBar. Now with VictoryHistogram, you can easily create histograms.

In Action

Let's take a look and see how you might go about using VictoryHistogram for visualizing your data.

Here's a basic example. We have our array of data, and we pass that into VictoryHistogram which automatically bins these values into four bins. You can customize this behavior via the bins prop, with which you can specify specific bin ranges or an approximate number of bins.

const App = () => {
  const data = [
    { x: 0 },
    { x: 1 },
    { x: 1 },
    { x: 2 },
    { x: 3 },
    { x: 4 },
    { x: 4 }
  ];

  return (
    <VictoryChart>
      <VictoryHistogram data={data} />
    </VictoryChart>
  );
};

Let's add some styling to these bins using the style and cornerRadius props.

const App = () => {
  const data = [
    { x: 0 },
    { x: 1 },
    { x: 1 },
    { x: 2 },
    { x: 3 },
    { x: 4 },
    { x: 4 }
  ];

  return (
    <VictoryChart>
      <VictoryHistogram
        style={{
          data: {
            fill: "hsl(355, 88%, 67%)",
            stroke: "hsl(355, 10%, 25%)"
          }
        }}
        cornerRadius={5}
        data={data}
      />
    </VictoryChart>
  );
};

Let's try using some more interesting data. In this case the data represents songs listened to on a particular date for a particular person. So each object represents one song, and contains the date it was listened to and the genre of the song.

So we pass this data to VictoryHistogram and we specify that the x value is located at the day property of each object by passing the prop, x="day"

import data from "./data.js"; // [{ day: new Date(2020, 3, 1), genre: 'hip-hop' }, ...]

const App = () => {
  return (
    <VictoryChart>
      <VictoryHistogram
        style={{
          data: {
            fill: "hsl(355, 88%, 67%)",
            stroke: "hsl(355, 10%, 25%)"
          }
        }}
        cornerRadius={5}
        data={data}
        x="day"
      />
    </VictoryChart>
  );
};

Let's add a title using VictoryLabel. Let's also update our axes by rendering two <VictoryAxis /> components, and using tickFormat and tickCount to display all of the month tick labels on the x-axis, and add a label to the y-axis via the label prop.

import data from "./data.js"; // [{ day: new Date(2020, 3, 1), genre: 'hip-hop' }, ...]

const sharedAxisStyles = {
  tickLabels: {
    fontSize: 13
  },
  axisLabel: {
    padding: 39,
    fontSize: 13,
    fontStyle: "italic"
  }
};

const App = () => {
  return (
    <VictoryChart scale={{ x: "time" }} style={{ parent: { padding: 36 } }}>
      <VictoryLabel
        x={225}
        y={30}
        textAnchor="middle"
        text="Songs listened to in 2020"
      />

      <VictoryHistogram
        style={{
          data: {
            fill: "hsl(355, 88%, 67%)",
            stroke: "hsl(355, 10%, 25%)"
          }
        }}
        cornerRadius={5}
        data={data}
        x="day"
      />

      <VictoryAxis
        tickCount={12}
        tickFormat={date => date.toLocaleString("default", { month: "short" })}
        style={sharedAxisStyles}
      />

      <VictoryAxis
        dependentAxis
        label="Total # of Songs"
        style={sharedAxisStyles}
      />
    </VictoryChart>
  );
};

Right now each bar represents the total songs listened to in a month, but we don't really see the breakdown of the songs that are included in that month. Luckily we have the genre of each song. So we can split up our data by genre using lodash groupBy (or whatever method you prefer). Then render a VictoryStack component and inside of that, we render a VictoryHistogram for each genre we have.

import data from "./data.js"; // [{ day: new Date(2020, 3, 1), genre: 'hip-hop' }, ...]
const groupedData = _.groupBy(data, ({ genre }) => genre); 
/* 
  { 
    rock: [{ day: new Date(2020, 3, 1), genre: 'rock' }, ...], 
    hip-hop: [{ day: new Date(2020, 3, 1), genre: 'hip-hop' }, ...] 
    ...
  }
*/

const sharedAxisStyles = {
  tickLabels: {
    fontSize: 13
  },
  axisLabel: {
    padding: 39,
    fontSize: 13,
    fontStyle: "italic"
  }
};

const App = () => {
  return (
    <VictoryChart scale={{ x: "time" }} style={{ parent: { padding: 36 } }}>
      <VictoryLabel
        x={225}
        y={25}
        textAnchor="middle"
        text="Songs listened to in 2020"
      />

      <VictoryStack
        colorScale={[
          "#003f5c",
          "#2f4b7c",
          "#665191",
          "#a05195",
          "#d45087",
          "#f95d6a",
          "#ff7c43",
          "#ffa600"
        ]}
      >
        {Object.entries(groupedData).map(([key, dataGroup]) => {
          return (
            <VictoryHistogram
              data={dataGroup}
              x="day"
              style={{
                data: { strokeWidth: 1, stroke: "hsl(324, 10%, 20%)" }
              }}
            />
          );
        })}
      </VictoryStack>

      <VictoryAxis
        tickCount={12}
        tickFormat={date => date.toLocaleString("default", { month: "short" })}
        style={sharedAxisStyles}
      />

      <VictoryAxis
        dependentAxis
        label="Total # of Songs"
        style={sharedAxisStyles}
      />
    </VictoryChart>
  );
};

We can tweak the styling again a little more, remove the borders via the style prop, and space the bins out a bit using the binSpacing prop.

import data from "./data.js"; // [{ day: new Date(2020, 3, 1), genre: 'hip-hop' }, ...]
const groupedData = _.groupBy(data, ({ genre }) => genre); 
/* 
  { 
    rock: [{ day: new Date(2020, 3, 1), genre: 'rock' }, ...], 
    hip-hop: [{ day: new Date(2020, 3, 1), genre: 'hip-hop' }, ...] 
    ...
  }
*/

const sharedAxisStyles = {
  tickLabels: {
    fontSize: 13
  },
  axisLabel: {
    padding: 39,
    fontSize: 13,
    fontStyle: "italic"
  }
};

const App = () => {
  return (
    <VictoryChart scale={{ x: "time" }} style={{ parent: { padding: 36 } }}>
      <VictoryLabel
        x={225}
        y={25}
        textAnchor="middle"
        text="Songs listened to in 2020"
      />

      <VictoryStack
        colorScale={[
          "#003f5c",
          "#2f4b7c",
          "#665191",
          "#a05195",
          "#d45087",
          "#f95d6a",
          "#ff7c43",
          "#ffa600"
        ]}
      >
        {Object.entries(groupedData).map(([key, dataGroup]) => {
          return (
            <VictoryHistogram
              data={dataGroup}
              x="day"
              binSpacing={8}
              style={{
                data: { strokeWidth: 0 }
              }}
            />
          );
        })}
      </VictoryStack>

      <VictoryAxis
        tickCount={12}
        tickFormat={date => date.toLocaleString("default", { month: "short" })}
        style={sharedAxisStyles}
      />

      <VictoryAxis
        dependentAxis
        label="Total # of Songs"
        style={sharedAxisStyles}
      />
    </VictoryChart>
  );
};

Lastly, we may want to add some tooltips so we can see the values of the data we are rendering. To do that we provide a containerComponent prop to VictoryChart. In this case we'll want to pass a VictoryVoronoiContainer component, which is used to associate a user's mouse position with the nearest data point. Then we add a labels prop to determine the text inside of the tooltip.

import data from "./data.js";
const groupedData = _.groupBy(data, ({ genre }) => genre);

const sharedAxisStyles = {
  tickLabels: {
    fontSize: 13
  },
  axisLabel: {
    padding: 39,
    fontSize: 13,
    fontStyle: "italic"
  }
};

const App = () => {
  return (
    <VictoryChart
      scale={{ x: "time" }}
      containerComponent={
        <VictoryVoronoiContainer
          labels={({ datum }) =>
            datum.y > 0 ? `${datum.y} ${datum.binnedData[0].genre} songs` : null
          }
        />
      }
    >
      <VictoryLabel
        x={225}
        y={25}
        textAnchor="middle"
        text="Songs listened to in 2020"
      />

      <VictoryStack
        colorScale={[
          "#003f5c",
          "#2f4b7c",
          "#665191",
          "#a05195",
          "#d45087",
          "#f95d6a",
          "#ff7c43",
          "#ffa600"
        ]}
      >
        {Object.entries(groupedData).map(([key, dataGroup]) => {
          return (
            <VictoryHistogram
              data={dataGroup}
              x="day"
              binSpacing={8}
              style={{
                data: { strokeWidth: 0 }
              }}
            />
          );
        })}
      </VictoryStack>

      <VictoryAxis
        tickCount={12}
        tickFormat={date => date.toLocaleString("default", { month: "short" })}
        style={sharedAxisStyles}
      />

      <VictoryAxis
        dependentAxis
        label="Total # of Songs"
        style={sharedAxisStyles}
      />
    </VictoryChart>
  );
};

To see this example, check out the link here:

Coming Up

In our next Victory blog post, we'll be kicking off a new blog post series around building advanced sports visualizations with Victory, like the one shown below.

Related Posts

Progress Towards OSS Sustainability

Open source sustainability has been an increasingly visible problem in recent years. The problem is too big for any one person, or any one company to ...

Victory is Turning 30

Victory is making the change to major versions with the release of victory@30.0.0! This is an unusual version bump, and a major milestone for the proj ...

Retail in the Time of Coronavirus: Today and Beyon ...

Current pandemic conditions have rapidly accelerated the rate of adoption of digital products in the retail industry. What happens when the shops reop ...

Check out more of Alex's blog posts