javascriptreactjsnivo-slider

Nivo bar chart calling label function hundreds of times


I'm using Nivo bar to represent a user's progress on a budget. I've normalized the data by dividing the category balance by the category goal. Example data.

[{
    "category": "Gas",
    "budget": 0.24,
    "over_budget": 0.0
},
{
    "category": "Groceries",
    "budget": 1.0,
    "over_budget": 0.26
}]

I don't want these values to be used as the label on the chart. I plan to use the actual balance value as the label. I have an endpoint that will return the balance for a category and have attempted the following to use that value:

<ResponsiveBar
...
label={d => this.getDollarAmount(d.value)}
...
>

With the function POC as:

getDollarAmount(value) {
    console.log("hitting getDollarAmount")
    return 1
  };

The log message gets logged 500+ times. My expectation would be that the function would only be hit once for each bar in the chart.

I'm still learning react so this could be something obvious. Thanks in advance!

EDIT - Here's the entire BarChart component:

import axios from 'axios';

import React, { Component } from "react";

import { ResponsiveBar } from '@nivo/bar'

// Nivo theming
const theme = {
  axis: {
    ticks: {
      line: {
        stroke: "#e9ecee",
        strokeWidth: 40
      },
      text: {
        // fill: "#919eab",
        fill: "black",
        fontFamily: "BlinkMacSystemFont",
        fontSize: 16
      }
    }
  },
  grid: {
    line: {
      stroke: "#e9ecee",
      strokeWidth: 5
    }
  },
  legends: {
    text: {
      fontFamily: "BlinkMacSystemFont"
    }
  }
};

let budgetStatusAPI = 'http://127.0.0.1:8000/api/budget_status/?auth_user=1&month=2020-02-01';

class BarChart extends Component {

  constructor(props) {
    super(props);

    this.state = {
      data: [],
    }

    this.getDollarAmount = this.getDollarAmount.bind(this);
  }


  componentDidMount() {
    console.log("component did mount")

    axios.get(budgetStatusAPI).then(response => {
      this.setState({
        data: response.data
      }, function () {
        console.log(this.state.data);
      })
    });
  }

  componentDidUpdate() {
    console.log("component did update")
  }

  getDollarAmount(value) {
    console.log("hitting getDollarAmount")
    console.log(value)
    return 1
  };


  render() {

    const hard_data = [
        {
          "category": "Groceries",
          "budget_status": 1.0,
          "over_budget": .26,
        },
        {
          "category": "Gas",
          "budget_status": .24,
          "over_budget": 0.0,
        }]

    return(

      <ResponsiveBar
        maxValue={1.5}
        markers={[
            {
                axis: 'x',
                value: 1,
                lineStyle: { stroke: 'rgba(0, 0, 0, .35)', strokeWidth: 2 },
                legend: 'Goal',
                legendOrientation: 'horizontal',
                legendPosition: 'top'
            },
        ]}
        enableGridX={false}
        gridXValues={[1]}
        enableGridY={false}
        data={this.state.data}
        // data={hard_data}
        keys={['budget_status', 'over_budget']}
        indexBy="category"
        margin={{ top: 25, right: 130, bottom: 50, left: 125 }}
        padding={0.3}
        layout="horizontal"
        colors={{ scheme: 'set2' }}
        theme={theme}
        defs={[
            {
                id: 'dots',
                type: 'patternDots',
                background: 'inherit',
                color: '#38bcb2',
                size: 4,
                padding: 1,
                stagger: true
            },
            {
                id: 'lines',
                type: 'patternLines',
                background: 'inherit',
                color: '#eed312',
                rotation: -45,
                lineWidth: 6,
                spacing: 10
            }
        ]}
        borderColor={{ from: 'color', modifiers: [ [ 'darker', 1.6 ] ] }}
        axisBottom={null}
        label={d => this.getDollarAmount(d.value)}
        isInteractive={false}
        animate={true}
        motionStiffness={90}
        motionDamping={15}
    />
    )
  }
}


export default BarChart;

Reproduced here: https://codesandbox.io/s/nivo-bar-label-issue-k4qek


Solution

  • The multiple calling is happening because the bar chart is calling label function for each animation tick/frame render. If we setup a counter, we'll see with animate prop set to true it will render from 450+ to 550+ times, but if we set the prop animate to false, we'll it renders 6 times which is exactly how many price values are > 0.0.

    If you want to avoid all these renders, you'll have to disable animation using animate={false} prop like this:

    getDollarAmount(value) {
      // Remove every console.log inside this function
    
      return `$${value}`
    }
    
    render() {
      return (
        <ResponsiveBar
          animate={false}
          label={d => this.getDollarAmount(d.value)}
          ...
      );
    }
    

    You can check it running to your cloned CodeSandbox. I have set animate to false and the counter log inside getDollarAmount is calling 6 times. Try to change animate to true and you'll see the 500+- renders.

    Also, you don't have to create a function for each label call, you can just pass the getDollarAmount function and let it handle the whole d parameter like this:

    getDollarAmount(d) {
      // Remove every console.log inside this function
    
      return `$${d.value}`
    }
    
    render() {
      return (
        <ResponsiveBar
          animate={false}
          label={this.getDollarAmount}
          ...
      );
    }