Search code examples
d3.js

d3.js restyling gauge chart with a filled circle rounded edges tip


I have a gauge chart that currently has a dial - how do I modify this to arc it back further and round the edges, and create a filled circle at the tip of the arc?

https://codesandbox.io/s/blissful-framework-chlptz?file=/src/GaugeChart.js

enter image description here

currently the chart I have looks like this with a dial. Restyling the current chart code. enter image description here

import React from 'react'
import * as d3 from 'd3'

class GaugeChart extends React.Component {
  constructor(props) {
    super(props)
    this.myRef = React.createRef()
    this.state = {
      data: [],
      theme: this.props.theme
        ? this.props.theme
        : ['#bde0fe', '#2698f9', '#71bcfd', '#f1f8fe'],
    }
  }

  componentDidMount() {
    var $this = this.myRef.current

    d3.select($this).selectAll('svg').remove()

    const value = this.props.value
    const data = [
      {
        label: 'Completed',
        value: value,
      },
      {
        label: 'Remaining',
        value: 100 - value,
      },
    ]

    const width = parseInt(this.props.width, 10),
      height = parseInt(this.props.height, 10),
      radius = parseInt(this.props.r, 10),
      innerradius = parseInt(this.props.ir, 10)

    var color = d3.scaleOrdinal().range(this.state.theme)

    var arc = d3.arc().outerRadius(radius).innerRadius(innerradius)

    data.forEach(function (d) {
      d.total = +d.value
    })

    var pie = d3
      .pie()
      .startAngle(-90 * (Math.PI / 180))
      .endAngle(90 * (Math.PI / 180))
      .padAngle(0.02) // some space between slices
      .sort(null)
      .value(function (d) {
        return d.total
      })

    var svg = d3
      .select($this)
      .append('svg')
      .attr('width', width)
      .attr('height', height + 5)
      .append('g')
      .attr('class', 'piechart')
      .attr('transform', 'translate(' + width / 2 + ',' + height + ')')

    var segments = svg.append('g').attr('class', 'segments')

    var slices = segments
      .selectAll('.arc')
      .data(pie(data))
      .enter()
      .append('g')
      .attr('class', 'arc')

    slices
      .append('path')
      .attr('d', arc)
      .attr('fill', function (d, i) {
        return color(i)
      })
    /*.transition()
          .attrTween('d', function(d) {
            var i = d3.interpolate(d.startAngle + 0.1, d.endAngle);
            return function(t) {
              d.endAngle = i(t);
              return arc(d);
            }
          } )*/

    var arrow = svg.append('g').attr('transform', 'rotate( 0  )')

    arrow
      .append('path')
      .attr('fill', '#ef7e60')
      .attr(
        'd',
        `M -${innerradius - 5},0 -${innerradius - 12},-5 -${
          innerradius - 12
        },5`,
      ) // draw triangle

    var text = svg
      .append('text')
      .attr('text-anchor', 'middle')
      .attr('dy', 0)
      .attr('fill', '#ef7e60')

    function update(data) {
      text.text(`${Math.round(data[0].value)}%`) // set % text

      arrow.transition().attr('transform', `rotate( ${data[0].value * 1.8}  )`) //rotate to desired qantity ( i put to 60% of 180 deg )
    }
    update(data)
  }

  render() {
    return <div ref={this.myRef} className="GaugeChart" />
  }
}
export default GaugeChart

latest fork https://codesandbox.io/s/reverent-hodgkin-z3dn4y


https://codesandbox.io/p/sandbox/hardcore-pare-nl95p4?file=%2Fsrc%2FGaugeChart.js%3A1%2C1-160%2C26 latest fork 1st December

import React from 'react'
import * as d3 from 'd3'

class GaugeChart extends React.Component {
  constructor(props) {
    super(props)
    this.myRef = React.createRef()
    this.state = {
      data: [],
      theme: this.props.theme
        ? this.props.theme
        : ['#bde0fe', '#2698f9', '#71bcfd', '#f1f8fe'],
    }
  }

  componentDidMount() {
    var $this = this.myRef.current

    d3.select($this).selectAll('svg').remove()

    const value = this.props.value
    const data = [
      {
        sortIndex: 0,
        value: value,
        fill: 'blue',
      },
      {
        sortIndex: 0,
        value: 100 - value,
        fill: '#EAEDF6',
      },
    ]
    let arcThickness = 4
    let size = 50
    let transitionDuration = 750

    const t = d3.transition().duration(350)
    const arc = d3
      .arc()
      .innerRadius(size / 2 - arcThickness)
      .outerRadius(size / 2)
      .cornerRadius(arcThickness)

    const pie = d3
      .pie()
      .value((d) => d.value)
      .startAngle(-Math.PI / 1.45)
      .endAngle(Math.PI / 1.45)
      .sort((a, b) => d3.ascending(a.sortIndex, b.sortIndex))

    const initData = pie(JSON.parse(JSON.stringify(data)))
    const arcs = pie(data)

    function arcTween(d, bIsLastArc) {
      var interpolateStart = d3.interpolate(-Math.PI / 1.45, d.startAngle)
      var interpolateEnd = d3.interpolate(
        bIsLastArc ? d.endAngle : -Math.PI / 1.45,
        d.endAngle,
      )

      return function (t) {
        d.startAngle = interpolateStart(t)
        d.endAngle = interpolateEnd(t)
        return arc(d)
      }
    }
    const svg = d3
      .select($this)
      .append('svg')
      .attr('viewBox', [-size / 2, -size / 2, size, size])
    // add def and drop shadow for the value dot
    svg.append('defs')
      .html(`<filter id="dot-shadow" x="-50%" y="-50%" width="200%" height="200%" filterUnits="objectBoundingBox">
      <feDropShadow dx="3" dy="3" stdDeviation="8" flood-color="${initData[0].data.fill}" flood-opacity="0.25" />
    </filter>`)

    // gauge chart skeleton

    const arcSkeleton = d3
      .arc()
      .innerRadius(size / 2 - arcThickness)
      .outerRadius(size / 2)
      .startAngle(-90 * (Math.PI/131))
      .cornerRadius(arcThickness)
      .endAngle(90 * (Math.PI/131))

    svg
      .append('path')
      .attr('class', 'arc-skeleton')
      .attr('d', arcSkeleton)
      .attr('fill', '#EAEDF6')

    const gauges = svg
      .selectAll('path.gaugechart')
      .data(arcs)
      .join('path')
      .attr('class', 'gaugechart')
      .attr('fill', (d) => d.data.fill)
      .attr('d', arc)

    gauges
      .transition()
      .duration(transitionDuration)
      .attrTween('d', (d, i) => arcTween(d, i == arcs.length - 1))

    // Add a dot to mark the end of the first arc
    const dot = svg
      .selectAll('circle')
      .data([
        Object.assign(initData[0], {
          startAngle: initData[0].endAngle - 0.2,
          endAngle: initData[1].startAngle,
          stroke: initData[initData.length - 1].data.fill,
        }),
      ])
      .join('circle')
      .attr('fill', '#fff')
      .attr('r', arcThickness * 0.3)
      .attr('stroke', (d) => d.stroke)
      .attr('stroke-width', 0.1)
      .attr('style', 'filter:url(#dot-shadow)')
      .attr('transform', (d) => `translate(${arc.centroid(d)})`)

    dot
      .transition()
      .duration(transitionDuration)
      .attrTween('transform', (d) => {
        const interpolateStart = d3.interpolate(-Math.PI / 1.45, d.startAngle)
        const interpolateEnd = d3.interpolate(-Math.PI / 1.45, d.endAngle)
        return function (t) {
          d.startAngle = interpolateStart(t)
          d.endAngle = interpolateEnd(t)
          return `translate(${arc.centroid(d)})`
        }
      })

    svg
      .append('text')
      .attr('transform', `translate(0, ${(0.1 * size) / 2})`)
      .attr('font-size', '0.7rem')
      .attr('text-anchor', 'middle')
      .attr('fill', 'black')
      .text(data[0].value)

    svg
      .append('text')
      .attr('transform', `translate(0, ${0.15 * size})`)
      .attr('font-size', '0.2rem')
      .attr('text-anchor', 'middle')
      .attr('fill', 'grey')
      .text('Volts')
  }

  render() {
    return <div ref={this.myRef} className="GaugeChart" />
  }
}
export default GaugeChart

Solution

  • 
    import React from 'react'
    import * as d3 from 'd3'
    
    class GaugeChart extends React.Component {
      constructor(props) {
        super(props)
        this.myRef = React.createRef()
        this.state = {
          data: [],
          theme: this.props.theme
            ? this.props.theme
            : ['#bde0fe', '#2698f9', '#71bcfd', '#f1f8fe'],
        }
      }
    
      componentDidMount() {
        var $this = this.myRef.current
    
        d3.select($this).selectAll('svg').remove()
    
        const value = this.props.value
        const data = [
          {
            sortIndex: 0,
            value: value,
            fill: 'blue',
          },
          {
            sortIndex: 0,
            value: 100 - value,
            fill: '#EAEDF6',
          },
        ]
        let arcThickness = 4
        let size = 50
        let transitionDuration = 750
    
        const t = d3.transition().duration(350)
        const arc = d3
          .arc()
          .innerRadius(size / 2 - arcThickness)
          .outerRadius(size / 2)
          .cornerRadius(arcThickness)
    
        const pie = d3
          .pie()
          .value((d) => d.value)
          .startAngle(-Math.PI / 1.45)
          .endAngle(Math.PI / 1.45)
          .sort((a, b) => d3.ascending(a.sortIndex, b.sortIndex))
    
        const initData = pie(JSON.parse(JSON.stringify(data)))
        const arcs = pie(data)
    
        function arcTween(d, bIsLastArc) {
          var interpolateStart = d3.interpolate(-Math.PI / 1.45, d.startAngle)
          var interpolateEnd = d3.interpolate(
            bIsLastArc ? d.endAngle : -Math.PI / 1.45,
            d.endAngle,
          )
    
          return function (t) {
            d.startAngle = interpolateStart(t)
            d.endAngle = interpolateEnd(t)
            return arc(d)
          }
        }
        const svg = d3
          .select($this)
          .append('svg')
          .attr('viewBox', [-size / 2, -size / 2, size, size])
        // add def and drop shadow for the value dot
        svg.append('defs')
          .html(`<filter id="dot-shadow" x="-50%" y="-50%" width="200%" height="200%" filterUnits="objectBoundingBox">
          <feDropShadow dx="3" dy="3" stdDeviation="8" flood-color="${initData[0].data.fill}" flood-opacity="0.25" />
        </filter>`)
    
        // gauge chart skeleton
    
        const arcSkeleton = d3
          .arc()
          .innerRadius(size / 2 - arcThickness)
          .outerRadius(size / 2)
          .startAngle(-90 * (Math.PI/131))
          .cornerRadius(arcThickness)
          .endAngle(90 * (Math.PI/131))
    
        svg
          .append('path')
          .attr('class', 'arc-skeleton')
          .attr('d', arcSkeleton)
          .attr('fill', '#EAEDF6')
    
        const gauges = svg
          .selectAll('path.gaugechart')
          .data(arcs)
          .join('path')
          .attr('class', 'gaugechart')
          .attr('fill', (d) => d.data.fill)
          .attr('d', arc)
    
        gauges
          .transition()
          .duration(transitionDuration)
          .attrTween('d', (d, i) => arcTween(d, i == arcs.length - 1))
    
        // Add a dot to mark the end of the first arc
        const dot = svg
          .selectAll('circle')
          .data([
            Object.assign(initData[0], {
              startAngle: initData[0].endAngle - 0.2,
              endAngle: initData[1].startAngle,
              stroke: initData[initData.length - 1].data.fill,
            }),
          ])
          .join('circle')
          .attr('fill', '#fff')
          .attr('r', arcThickness * 0.3)
          .attr('stroke', (d) => d.stroke)
          .attr('stroke-width', 0.1)
          .attr('style', 'filter:url(#dot-shadow)')
          .attr('transform', (d) => `translate(${arc.centroid(d)})`)
    
        dot
          .transition()
          .duration(transitionDuration)
          .attrTween('transform', (d) => {
            const interpolateStart = d3.interpolate(-Math.PI / 1.45, d.startAngle)
            const interpolateEnd = d3.interpolate(-Math.PI / 1.45, d.endAngle)
            return function (t) {
              d.startAngle = interpolateStart(t)
              d.endAngle = interpolateEnd(t)
              return `translate(${arc.centroid(d)})`
            }
          })
    
        svg
          .append('text')
          .attr('transform', `translate(0, ${(0.1 * size) / 2})`)
          .attr('font-size', '0.7rem')
          .attr('text-anchor', 'middle')
          .attr('fill', 'black')
          .text(data[0].value)
    
        svg
          .append('text')
          .attr('transform', `translate(0, ${0.15 * size})`)
          .attr('font-size', '0.2rem')
          .attr('text-anchor', 'middle')
          .attr('fill', 'grey')
          .text('Volts')
      }
    
      render() {
        return <div ref={this.myRef} className="GaugeChart" />
      }
    }
    export default GaugeChart
    
    

    Update your GaugeChart component according to above code snippets