Search code examples
d3.js

d3js rectangle strip gauge chart


enter image description here

How would I convert the current code to something like this pressure chart - where rectangles are arranged in an arc -- and then the best case tick is highlighted to represent the value.

So we would append rectangles and change the angle as we create the skeleton.

I've got this demo that creates a spread of rectangles and changes the angles - but I am not sure how to stabilize this and create the arc from the left side - https://jsfiddle.net/smo6kqzw/3/

var svg = d3.select('body').append('svg').attr("height", 500).attr("width", 500).attr("class", "svger")




var rectbuf = 50;

var padd = 1;

  
for (let i = 0; i < 20; i++) {

svg.append("rect")
  .attr("width", 30)
  .attr("height", 20)
  .attr("fill", "red")
  .attr("y", 20)
  .attr("x", rectbuf * 5)
  .attr("transform", "rotate("+((5+padd) *i)+")")

}
  

// create svg element:
var svg = d3.select("#rect").append("svg").attr("width", 800).attr("height", 200)

// Add the path using this helper function
svg.append('rect')
  .attr('x', 10)
  .attr('y', 120)
  .attr('width', 600)
  .attr('height', 40)
  .attr('stroke', 'black')
  .attr('fill', '#69a3b2');

I currently have a dial chart that looks like this 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 fiddle 2nd Dec https://jsfiddle.net/gp4wL80t/

latest fiddle 3rd Dec https://jsfiddle.net/0k7zvdxn/


Solution

  • Regarding your demo code re...

    "I've got this demo that creates a spread of rectangles and changes the angles - but I am not sure how to stabilize this and create the arc from the left side"

    Really you just need to modify the starting angle then calculate the positions for the ticks directly along the arc.

    EDIT

    You can make the bars thicker by using stroke-width, go around the edges more by adjusting the startAngle and endAngle (in radians).

    To highlight the nearest marker that would approximate the value, and also display the value in the middle/bottom like the pressure example - you just need to calculate the marker based on the value and display the text, etc - e.g.

    const value = Math.floor(Math.random() * 100); // value you want to show, random here just as an example...
    const numTicks = 50; // total ticks around the edge
    const lineThickness = 3; // width of ticks 
    const tickLength = 10; // length of ticks 
    const arcRadius = 55; // size of the arc
    const startAngle = -220 * (Math.PI / 180); // first tick pos in rad
    const endAngle = 40 * (Math.PI / 180); // last tick pos in rad
    const centerX = 75;
    const centerY = 75;
    const width = 150;
    const height = 150;
    
    const svg = d3.select('body')
      .append('svg')
      .attr("height", height)
      .attr("width", width)
      .attr("class", "svger");
    
    for (let i = 0; i < numTicks; i++) {
      const angle = startAngle + (i / (numTicks - 1)) * (endAngle - startAngle);
      const isHighlighted = i === Math.round((value / 100) * (numTicks - 1));
      const highlightAmmount = isHighlighted ? 2 : 0; // highlight sizing
      const x1 = centerX + arcRadius * Math.cos(angle);
      const y1 = centerY + arcRadius * Math.sin(angle);
      const x2 = x1 + (tickLength + highlightAmmount) * Math.cos(angle);
      const y2 = y1 + (tickLength + highlightAmmount) * Math.sin(angle);
      const tick = svg.append("line")
        .attr("x1", x1)
        .attr("y1", y1)
        .attr("x2", x2)
        .attr("y2", y2)
        .attr("stroke", isHighlighted ? "#ffffff" : '#6294d5') // highlight colour
        .attr("stroke-linecap", "round")
        .attr("stroke-width", lineThickness + highlightAmmount);
    }
    
    svg.append("text")
      .attr("x", centerX)
      .attr("y", centerY)
      .text(value)
      .attr("text-anchor", "middle")
      .attr("alignment-baseline", "middle")
      .attr("font-family", "Arial")
      .attr("font-size", "30px")
      .attr("font-weight", "bold")
      .attr("fill", "#ffffff");
    
    svg.append("text")
      .attr("x", 35)
      .attr("y", height - 15)
      .text("Low")
      .attr("text-anchor", "middle")
      .attr("alignment-baseline", "middle")
      .attr("font-family", "Arial")
      .attr("font-size", "12px")
      .attr("fill", "#ffffff");
    
    svg.append("text")
      .attr("x", width - 35)
      .attr("y", height - 15)
      .text("High")
      .attr("text-anchor", "middle")
      .attr("alignment-baseline", "middle")
      .attr("font-family", "Arial")
      .attr("font-size", "12px")
      .attr("fill", "#ffffff");
    .svger {
      background: #4c7dbb;
    }
    <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>

    I also styled it to look more like your example. Note the value I've displayed is random just to show how the highlighting is working. i.e. each time you click run code snippet a different value is shown :)

    I also made a fiddle if you want to play with it - https://jsfiddle.net/fraser/9nyc5eb1/